Remove Settings and Setup pages along with their tests and related API services
- Deleted Settings.tsx and Setup.tsx pages, which included functionality for changing passwords and setting up an admin account. - Removed associated test files for Setup page. - Eliminated API service definitions related to proxy hosts, remote servers, import functionality, and health checks. - Cleaned up mock data and test setup files. - Removed configuration files for TypeScript, Vite, and Tailwind CSS. - Deleted scripts for testing coverage, release management, Dockerfile validation, and Python compilation checks. - Removed Sourcery pre-commit wrapper script.
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
# Codecov configuration - require 75% overall coverage by default
|
||||
# Adjust target as needed
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 75%
|
||||
threshold: 0%
|
||||
|
||||
# Fail CI if Codecov upload/report indicates a problem
|
||||
require_ci_to_pass: yes
|
||||
|
||||
# Exclude folders from Codecov
|
||||
ignore:
|
||||
- "**/tests/*"
|
||||
- "**/test/*"
|
||||
- "**/__tests__/*"
|
||||
- "**/test_*.go"
|
||||
- "**/*_test.go"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "docs/*"
|
||||
- ".github/*"
|
||||
- "scripts/*"
|
||||
- "tools/*"
|
||||
- "frontend/node_modules/*"
|
||||
- "frontend/dist/*"
|
||||
- "frontend/coverage/*"
|
||||
- "backend/cmd/seed/*"
|
||||
- "backend/data/*"
|
||||
- "backend/coverage/*"
|
||||
- "*.md"
|
||||
@@ -1,76 +0,0 @@
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
.github/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
*.cover
|
||||
.hypothesis/
|
||||
htmlcov/
|
||||
*.egg-info/
|
||||
|
||||
# Node/Frontend build artifacts
|
||||
frontend/node_modules/
|
||||
frontend/coverage/
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
frontend/*.tsbuildinfo
|
||||
|
||||
# Go/Backend
|
||||
backend/api
|
||||
backend/*.out
|
||||
backend/coverage/
|
||||
backend/coverage.*.out
|
||||
|
||||
# Databases (runtime)
|
||||
backend/data/*.db
|
||||
backend/cmd/api/data/*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Environment
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
**/Dockerfile.*
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
.pre-commit-config.yaml
|
||||
|
||||
# Scripts
|
||||
scripts/
|
||||
tools/
|
||||
@@ -1,14 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
github: Wikid82
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: Wikid82
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,93 +0,0 @@
|
||||
name: 🏗️ Alpha Feature
|
||||
description: Create an issue for an Alpha milestone feature
|
||||
title: "[ALPHA] "
|
||||
labels: ["alpha", "feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Alpha Milestone Feature
|
||||
Features that are part of the core foundation and initial release.
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How critical is this feature?
|
||||
options:
|
||||
- Critical (Blocking, must-have)
|
||||
- High (Important, should have)
|
||||
- Medium (Nice to have)
|
||||
- Low (Future enhancement)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: issue_number
|
||||
attributes:
|
||||
label: Planning Issue Number
|
||||
description: Reference number from PROJECT_PLANNING.md (e.g., Issue #5)
|
||||
placeholder: "Issue #"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: What should this feature do?
|
||||
placeholder: Describe the feature in detail
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: tasks
|
||||
attributes:
|
||||
label: Implementation Tasks
|
||||
description: List of tasks to complete this feature
|
||||
placeholder: |
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
value: |
|
||||
- [ ]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance Criteria
|
||||
description: How do we know this feature is complete?
|
||||
placeholder: |
|
||||
- [ ] Criteria 1
|
||||
- [ ] Criteria 2
|
||||
value: |
|
||||
- [ ]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: categories
|
||||
attributes:
|
||||
label: Categories
|
||||
description: Select all that apply
|
||||
options:
|
||||
- label: Backend
|
||||
- label: Frontend
|
||||
- label: Database
|
||||
- label: Caddy Integration
|
||||
- label: Security
|
||||
- label: SSL/TLS
|
||||
- label: UI/UX
|
||||
- label: Deployment
|
||||
- label: Documentation
|
||||
|
||||
- type: textarea
|
||||
id: technical_notes
|
||||
attributes:
|
||||
label: Technical Notes
|
||||
description: Any technical considerations or dependencies?
|
||||
placeholder: Libraries, APIs, or other issues that need to be completed first
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,118 +0,0 @@
|
||||
name: 📊 Beta Monitoring Feature
|
||||
description: Create an issue for a Beta milestone monitoring/logging feature
|
||||
title: "[BETA] [MONITORING] "
|
||||
labels: ["beta", "feature", "monitoring"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Beta Monitoring & Logging Feature
|
||||
Features related to observability, logging, and system monitoring.
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How critical is this monitoring feature?
|
||||
options:
|
||||
- Critical (Essential for operations)
|
||||
- High (Important visibility)
|
||||
- Medium (Enhanced monitoring)
|
||||
- Low (Nice-to-have metrics)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: monitoring_type
|
||||
attributes:
|
||||
label: Monitoring Type
|
||||
description: What aspect of monitoring?
|
||||
options:
|
||||
- Dashboards & Statistics
|
||||
- Log Viewing & Search
|
||||
- Alerting & Notifications
|
||||
- CrowdSec Dashboard
|
||||
- Analytics Integration
|
||||
- Health Checks
|
||||
- Performance Metrics
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: issue_number
|
||||
attributes:
|
||||
label: Planning Issue Number
|
||||
description: Reference number from PROJECT_PLANNING.md (e.g., Issue #23)
|
||||
placeholder: "Issue #"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: What monitoring/logging capability should this provide?
|
||||
placeholder: Describe what users will be able to see or do
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: metrics
|
||||
attributes:
|
||||
label: Metrics & Data Points
|
||||
description: What data will be collected and displayed?
|
||||
placeholder: |
|
||||
- Metric 1: Description
|
||||
- Metric 2: Description
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: tasks
|
||||
attributes:
|
||||
label: Implementation Tasks
|
||||
description: List of tasks to complete this feature
|
||||
placeholder: |
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
value: |
|
||||
- [ ]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance Criteria
|
||||
description: How do we verify this monitoring feature works?
|
||||
placeholder: |
|
||||
- [ ] Data displays correctly
|
||||
- [ ] Updates in real-time
|
||||
- [ ] Performance is acceptable
|
||||
value: |
|
||||
- [ ]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: categories
|
||||
attributes:
|
||||
label: Implementation Areas
|
||||
description: Select all that apply
|
||||
options:
|
||||
- label: Backend (Data collection)
|
||||
- label: Frontend (UI/Charts)
|
||||
- label: Database (Storage)
|
||||
- label: Real-time Updates (WebSocket)
|
||||
- label: External Integration (GoAccess, CrowdSec)
|
||||
- label: Documentation Required
|
||||
|
||||
- type: textarea
|
||||
id: ui_design
|
||||
attributes:
|
||||
label: UI/UX Considerations
|
||||
description: Describe the user interface requirements
|
||||
placeholder: Layout, charts, filters, export options, etc.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,116 +0,0 @@
|
||||
name: 🔐 Beta Security Feature
|
||||
description: Create an issue for a Beta milestone security feature
|
||||
title: "[BETA] [SECURITY] "
|
||||
labels: ["beta", "feature", "security"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Beta Security Feature
|
||||
Advanced security features for the beta release.
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How critical is this security feature?
|
||||
options:
|
||||
- Critical (Essential security control)
|
||||
- High (Important protection)
|
||||
- Medium (Additional hardening)
|
||||
- Low (Nice-to-have security enhancement)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: security_category
|
||||
attributes:
|
||||
label: Security Category
|
||||
description: What type of security feature is this?
|
||||
options:
|
||||
- Authentication & Access Control
|
||||
- Threat Protection
|
||||
- SSL/TLS Management
|
||||
- Monitoring & Logging
|
||||
- Web Application Firewall
|
||||
- Rate Limiting
|
||||
- IP Access Control
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: issue_number
|
||||
attributes:
|
||||
label: Planning Issue Number
|
||||
description: Reference number from PROJECT_PLANNING.md (e.g., Issue #15)
|
||||
placeholder: "Issue #"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Feature Description
|
||||
description: What security capability should this provide?
|
||||
placeholder: Describe the security feature and its purpose
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: threat_model
|
||||
attributes:
|
||||
label: Threat Model
|
||||
description: What threats does this feature mitigate?
|
||||
placeholder: |
|
||||
- Threat 1: Description and severity
|
||||
- Threat 2: Description and severity
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: tasks
|
||||
attributes:
|
||||
label: Implementation Tasks
|
||||
description: List of tasks to complete this feature
|
||||
placeholder: |
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
value: |
|
||||
- [ ]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: Acceptance Criteria
|
||||
description: How do we verify this security control works?
|
||||
placeholder: |
|
||||
- [ ] Security test 1
|
||||
- [ ] Security test 2
|
||||
value: |
|
||||
- [ ]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: special_labels
|
||||
attributes:
|
||||
label: Special Categories
|
||||
description: Select all that apply
|
||||
options:
|
||||
- label: SSO (Single Sign-On)
|
||||
- label: WAF (Web Application Firewall)
|
||||
- label: CrowdSec Integration
|
||||
- label: Plus Feature (Premium)
|
||||
- label: Requires Documentation
|
||||
|
||||
- type: textarea
|
||||
id: security_testing
|
||||
attributes:
|
||||
label: Security Testing Plan
|
||||
description: How will you test this security feature?
|
||||
placeholder: Describe testing approach, tools, and scenarios
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,97 +0,0 @@
|
||||
name: ⚙️ General Feature
|
||||
description: Create a feature request for any milestone
|
||||
title: "[FEATURE] "
|
||||
labels: ["feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Feature Request
|
||||
Request a new feature or enhancement for CaddyProxyManager+
|
||||
|
||||
- type: dropdown
|
||||
id: milestone
|
||||
attributes:
|
||||
label: Target Milestone
|
||||
description: Which release should this be part of?
|
||||
options:
|
||||
- Alpha (Core foundation)
|
||||
- Beta (Advanced features)
|
||||
- Post-Beta (Future enhancements)
|
||||
- Unsure (Help me decide)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How important is this feature?
|
||||
options:
|
||||
- Critical
|
||||
- High
|
||||
- Medium
|
||||
- Low
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: What problem does this feature solve?
|
||||
placeholder: Describe the use case or pain point
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: How should this feature work?
|
||||
placeholder: Describe your ideal implementation
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: What other approaches could solve this?
|
||||
placeholder: List alternative solutions you've thought about
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: user_story
|
||||
attributes:
|
||||
label: User Story
|
||||
description: Describe this from a user's perspective
|
||||
placeholder: "As a [user type], I want to [action] so that [benefit]"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: categories
|
||||
attributes:
|
||||
label: Feature Categories
|
||||
description: Select all that apply
|
||||
options:
|
||||
- label: Authentication/Authorization
|
||||
- label: Security
|
||||
- label: SSL/TLS
|
||||
- label: Monitoring/Logging
|
||||
- label: UI/UX
|
||||
- label: Performance
|
||||
- label: Documentation
|
||||
- label: API
|
||||
- label: Plus Feature (Premium)
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other information, screenshots, or examples?
|
||||
placeholder: Add links, mockups, or references
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,38 +0,0 @@
|
||||
# CaddyProxyManager+ Copilot Instructions
|
||||
|
||||
## 🚨 CRITICAL ARCHITECTURE RULES 🚨
|
||||
- **Single Frontend Source**: All frontend code MUST reside in `frontend/`. NEVER create `backend/frontend/` or any other nested frontend directory.
|
||||
- **Single Backend Source**: All backend code MUST reside in `backend/`.
|
||||
- **No Python**: This is a Go (Backend) + React/TypeScript (Frontend) project. Do not introduce Python scripts or requirements.
|
||||
|
||||
## Big Picture
|
||||
- `backend/cmd/api` loads config, opens SQLite, then hands off to `internal/server` where routes from `internal/api/routes` are registered.
|
||||
- `internal/config` respects `CPM_ENV`, `CPM_HTTP_PORT`, `CPM_DB_PATH`, `CPM_FRONTEND_DIR` and creates the `data/` directory; lean on these instead of hard-coded paths.
|
||||
- All HTTP endpoints live under `/api/v1/*`; keep new handlers inside `internal/api/handlers` and register them via `routes.Register` so `db.AutoMigrate` runs for their models.
|
||||
- `internal/server` also mounts the built React app (via `attachFrontend`) whenever `frontend/dist` exists, falling back to JSON `{"error": ...}` for any `/api/*` misses.
|
||||
- Persistent types live in `internal/models`; GORM auto-migrates them each boot, so evolve schemas there before touching handlers or the frontend.
|
||||
|
||||
## Backend Workflow
|
||||
- Run locally with `cd backend && go run ./cmd/api`; run tests with `go test ./...` (see `proxy_host_handler_test.go` for the in-memory SQLite/Gin harness pattern).
|
||||
- Handlers return structured errors using `gin.H{"error": "message"}` and standard HTTP codes—mirror the `ProxyHostHandler` lifecycle for new CRUD endpoints.
|
||||
- UUIDs (`github.com/google/uuid`) are generated server-side and exposed as `uuid` fields; clients never send numeric IDs.
|
||||
- Query lists sorted by `updated_at desc` (see `.Order("updated_at desc")` in `List`); match that ordering for user-visible collections.
|
||||
- Long-running work must respect the graceful shutdown flow in `server.Run(ctx)`—avoid background goroutines that ignore the context.
|
||||
|
||||
## Frontend Workflow
|
||||
- **Location**: Always work within `frontend/`.
|
||||
- **Stack**: React 18 + Vite + TypeScript + TanStack Query (React Query).
|
||||
- **State Management**: Use `src/hooks/use*.ts` wrapping React Query. Do not use raw `useEffect` for data fetching.
|
||||
- **API Layer**: Create typed API clients in `src/api/*.ts` that wrap `client.ts`.
|
||||
- **Development**: Run `cd frontend && npm run dev`. Vite proxies `/api` to `http://localhost:8080`.
|
||||
- **Components**: Screens live in `src/pages`. Reusable UI in `src/components`.
|
||||
- **Forms**: Use local `useState` for form fields, submit via `useMutation` from custom hooks, then `invalidateQueries` on success.
|
||||
|
||||
## Cross-Cutting Notes
|
||||
- Run the backend before the frontend; React Query expects the exact JSON produced by GORM tags (snake_case), so keep API and UI field names aligned.
|
||||
- When adding models, update both `internal/models` and the `AutoMigrate` call inside `internal/api/routes/routes.go`; register new Gin routes right after migrations for clarity.
|
||||
- Tests belong beside handlers (`*_test.go`); reuse the `setupTestRouter` helper structure (in-memory SQLite, Gin router, httptest requests) for fast feedback.
|
||||
- **Testing Requirement**: All new code (features, bug fixes, refactors) MUST include accompanying unit tests. Ensure tests cover happy paths and error conditions.
|
||||
- **Ignore Files**: When creating new file types, directories, or build artifacts, ALWAYS check and update `.gitignore`, `.dockerignore`, and `.codecov.yml` to ensure they are properly excluded or included as required.
|
||||
- The root `Dockerfile` builds the Go binary and the React static assets (multi-stage build).
|
||||
- Branch from `feature/**` and target `development`.
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticCommits",
|
||||
":separateMultipleMajorReleases",
|
||||
"helpers:pinGitHubActionDigests"
|
||||
],
|
||||
"baseBranches": ["development"],
|
||||
"timezone": "UTC",
|
||||
"dependencyDashboard": true,
|
||||
"prConcurrentLimit": 10,
|
||||
"prHourlyLimit": 5,
|
||||
"labels": ["dependencies"],
|
||||
"rebaseWhen": "conflicted",
|
||||
"vulnerabilityAlerts": { "enabled": true },
|
||||
"schedule": ["every weekday"],
|
||||
"rangeStrategy": "bump",
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Automerge safe patch updates",
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Frontend npm: automerge minor for devDependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true,
|
||||
"labels": ["dependencies", "npm"]
|
||||
},
|
||||
{
|
||||
"description": "Backend Go modules",
|
||||
"matchManagers": ["gomod"],
|
||||
"labels": ["dependencies", "go"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "GitHub Actions updates",
|
||||
"matchManagers": ["github-actions"],
|
||||
"labels": ["dependencies", "github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Docker: keep Caddy within v2 (no automatic jump to v3)",
|
||||
"matchManagers": ["dockerfile"],
|
||||
"matchPackageNames": ["caddy"],
|
||||
"allowedVersions": "<3.0.0",
|
||||
"labels": ["dependencies", "docker"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group non-breaking npm minor/patch",
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "npm minor/patch",
|
||||
"prPriority": -1
|
||||
},
|
||||
{
|
||||
"description": "Group docker base minor/patch",
|
||||
"matchManagers": ["dockerfile"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "docker base updates",
|
||||
"prPriority": -1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Auto-add issues and PRs to Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine project URL presence
|
||||
id: project_check
|
||||
run: |
|
||||
if [ -n "${{ secrets.PROJECT_URL }}" ]; then
|
||||
echo "has_project=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_project=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Add issue or PR to project
|
||||
if: steps.project_check.outputs.has_project == 'true'
|
||||
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
project-url: ${{ secrets.PROJECT_URL }}
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
|
||||
- name: Skip summary
|
||||
if: steps.project_check.outputs.has_project == 'false'
|
||||
run: echo "PROJECT_URL secret missing; skipping project assignment." >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Auto-label Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label based on title and body
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title.toLowerCase();
|
||||
const body = issue.body ? issue.body.toLowerCase() : '';
|
||||
const labels = [];
|
||||
|
||||
// Priority detection
|
||||
if (title.includes('[critical]') || body.includes('priority: critical')) {
|
||||
labels.push('critical');
|
||||
} else if (title.includes('[high]') || body.includes('priority: high')) {
|
||||
labels.push('high');
|
||||
} else if (title.includes('[medium]') || body.includes('priority: medium')) {
|
||||
labels.push('medium');
|
||||
} else if (title.includes('[low]') || body.includes('priority: low')) {
|
||||
labels.push('low');
|
||||
}
|
||||
|
||||
// Milestone detection
|
||||
if (title.includes('[alpha]') || body.includes('milestone: alpha')) {
|
||||
labels.push('alpha');
|
||||
} else if (title.includes('[beta]') || body.includes('milestone: beta')) {
|
||||
labels.push('beta');
|
||||
} else if (title.includes('[post-beta]') || body.includes('milestone: post-beta')) {
|
||||
labels.push('post-beta');
|
||||
}
|
||||
|
||||
// Category detection
|
||||
if (title.includes('architecture') || body.includes('architecture')) labels.push('architecture');
|
||||
if (title.includes('backend') || body.includes('backend')) labels.push('backend');
|
||||
if (title.includes('frontend') || body.includes('frontend')) labels.push('frontend');
|
||||
if (title.includes('security') || body.includes('security')) labels.push('security');
|
||||
if (title.includes('ssl') || title.includes('tls') || body.includes('certificate')) labels.push('ssl');
|
||||
if (title.includes('sso') || body.includes('single sign-on')) labels.push('sso');
|
||||
if (title.includes('waf') || body.includes('web application firewall')) labels.push('waf');
|
||||
if (title.includes('crowdsec') || body.includes('crowdsec')) labels.push('crowdsec');
|
||||
if (title.includes('caddy') || body.includes('caddy')) labels.push('caddy');
|
||||
if (title.includes('database') || body.includes('database')) labels.push('database');
|
||||
if (title.includes('ui') || title.includes('interface')) labels.push('ui');
|
||||
if (title.includes('docker') || title.includes('deployment')) labels.push('deployment');
|
||||
if (title.includes('monitoring') || title.includes('logging')) labels.push('monitoring');
|
||||
if (title.includes('documentation') || title.includes('docs')) labels.push('documentation');
|
||||
if (title.includes('test') || body.includes('testing')) labels.push('testing');
|
||||
if (title.includes('performance') || body.includes('optimization')) labels.push('performance');
|
||||
if (title.includes('plus') || body.includes('premium feature')) labels.push('plus');
|
||||
|
||||
// Feature detection
|
||||
if (title.includes('feature') || body.includes('feature request')) labels.push('feature');
|
||||
|
||||
// Only add labels if we detected any
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
|
||||
console.log(`Added labels: ${labels.join(', ')}`);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Monitor Caddy Major Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 7 * * 1' # Mondays at 07:17 UTC
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
check-caddy-major:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for Caddy v3 and open issue
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const upstream = { owner: 'caddyserver', repo: 'caddy' };
|
||||
const { data: releases } = await github.rest.repos.listReleases({
|
||||
...upstream,
|
||||
per_page: 50,
|
||||
});
|
||||
const latestV3 = releases.find(r => /^v3\./.test(r.tag_name));
|
||||
if (!latestV3) {
|
||||
core.info('No Caddy v3 release detected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const issueTitle = `Track upgrade to Caddy v3 (${latestV3.tag_name})`;
|
||||
|
||||
const { data: existing } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
if (existing.some(i => i.title === issueTitle)) {
|
||||
core.info('Issue already exists — nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
'Caddy v3 has been released upstream and detected by the scheduled monitor.',
|
||||
'',
|
||||
`Detected release: ${latestV3.tag_name} (${latestV3.html_url})`,
|
||||
'',
|
||||
'- Create a feature branch to evaluate the v3 migration.',
|
||||
'- Review breaking changes and update Docker base images/workflows.',
|
||||
'- Validate Trivy scans and update any policies as needed.',
|
||||
'',
|
||||
'Current policy: remain on latest 2.x until v3 is validated.'
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: issueTitle,
|
||||
body,
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
name: CodeQL - Analyze
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, development, 'feature/**' ]
|
||||
pull_request:
|
||||
branches: [ main, development ]
|
||||
schedule:
|
||||
- cron: '0 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: CodeQL analysis (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
# Skip forked PRs where GITHUB_TOKEN lacks security-events permissions
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript-typescript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
@@ -1,78 +0,0 @@
|
||||
name: Create Project Labels
|
||||
|
||||
# This workflow only runs manually to set up labels
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-labels:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Create all project labels
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const labels = [
|
||||
// Priority labels
|
||||
{ name: 'critical', color: 'B60205', description: 'Must have for the release, blocks other work' },
|
||||
{ name: 'high', color: 'D93F0B', description: 'Important feature, should be included' },
|
||||
{ name: 'medium', color: 'FBCA04', description: 'Nice to have, can be deferred' },
|
||||
{ name: 'low', color: '0E8A16', description: 'Future enhancement, not urgent' },
|
||||
|
||||
// Milestone labels
|
||||
{ name: 'alpha', color: '5319E7', description: 'Part of initial alpha release' },
|
||||
{ name: 'beta', color: '0052CC', description: 'Part of beta release' },
|
||||
{ name: 'post-beta', color: '006B75', description: 'Post-beta enhancement' },
|
||||
|
||||
// Category labels
|
||||
{ name: 'architecture', color: 'C5DEF5', description: 'System design and structure' },
|
||||
{ name: 'backend', color: '1D76DB', description: 'Server-side code' },
|
||||
{ name: 'frontend', color: '5EBEFF', description: 'UI/UX code' },
|
||||
{ name: 'feature', color: 'A2EEEF', description: 'New functionality' },
|
||||
{ name: 'security', color: 'EE0701', description: 'Security-related' },
|
||||
{ name: 'ssl', color: 'F9D0C4', description: 'SSL/TLS certificates' },
|
||||
{ name: 'sso', color: 'D4C5F9', description: 'Single Sign-On' },
|
||||
{ name: 'waf', color: 'B60205', description: 'Web Application Firewall' },
|
||||
{ name: 'crowdsec', color: 'FF6B6B', description: 'CrowdSec integration' },
|
||||
{ name: 'caddy', color: '1F6FEB', description: 'Caddy-specific' },
|
||||
{ name: 'database', color: '006B75', description: 'Database-related' },
|
||||
{ name: 'ui', color: '7057FF', description: 'User interface' },
|
||||
{ name: 'deployment', color: '0E8A16', description: 'Docker, installation' },
|
||||
{ name: 'monitoring', color: 'FEF2C0', description: 'Logging and statistics' },
|
||||
{ name: 'documentation', color: '0075CA', description: 'Docs and guides' },
|
||||
{ name: 'testing', color: 'BFD4F2', description: 'Test suite' },
|
||||
{ name: 'performance', color: 'EDEDED', description: 'Optimization' },
|
||||
{ name: 'community', color: 'D876E3', description: 'Community building' },
|
||||
{ name: 'plus', color: 'FFD700', description: 'Premium/"Plus" feature' },
|
||||
{ name: 'enterprise', color: '8B4513', description: 'Enterprise-grade feature' }
|
||||
];
|
||||
|
||||
for (const label of labels) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description
|
||||
});
|
||||
console.log(`✓ Created label: ${label.name}`);
|
||||
} catch (error) {
|
||||
if (error.status === 422) {
|
||||
console.log(`⚠ Label already exists: ${label.name}`);
|
||||
// Update the label if it exists
|
||||
await github.rest.issues.updateLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description
|
||||
});
|
||||
console.log(`✓ Updated label: ${label.name}`);
|
||||
} else {
|
||||
console.error(`✗ Error creating label ${label.name}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Pushes to main → tags as "latest"
|
||||
- development # Pushes to development → tags as "dev"
|
||||
tags:
|
||||
- 'v*.*.*' # Version tags (v1.0.0, v1.2.3, etc.) → tags as version number
|
||||
workflow_dispatch: # Allows manual trigger from GitHub UI
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/cpmp
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: docker-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
outputs:
|
||||
skip_build: ${{ steps.skip.outputs.skip_build }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
# Step 1: Download the code
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- name: 🧪 Determine skip condition
|
||||
id: skip
|
||||
env:
|
||||
ACTOR: ${{ github.actor }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
HEAD_MSG: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
should_skip=false
|
||||
pr_title=""
|
||||
if [ "$EVENT" = "pull_request" ]; then
|
||||
pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '')
|
||||
fi
|
||||
if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
echo "skip_build=$should_skip" >> $GITHUB_OUTPUT
|
||||
if [ "$should_skip" = true ]; then
|
||||
echo "Skipping heavy docker build for actor=$ACTOR event=$EVENT (message/title matched)"
|
||||
else
|
||||
echo "Proceeding with full docker build"
|
||||
fi
|
||||
|
||||
# Step 2: Set up QEMU for multi-platform builds (ARM, AMD64, etc.)
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
|
||||
# Step 3: Set up Docker Buildx (advanced Docker builder)
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
|
||||
# Resolve immutable digest for Caddy base
|
||||
- name: 📦 Resolve Caddy base digest
|
||||
id: caddy
|
||||
run: |
|
||||
docker pull caddy:2-alpine
|
||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
|
||||
echo "image=$DIGEST" >> $GITHUB_OUTPUT
|
||||
|
||||
# Step 4: Log in to GitHub Container Registry
|
||||
- name: 🔐 Log in to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.PROJECT_TOKEN }}
|
||||
|
||||
# Step 5: Figure out what tags to use
|
||||
- name: 🏷️ Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
# Tag "latest" for main branch
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
# Tag "dev" for development branch
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
|
||||
# Tag version numbers from git tags (v1.0.0 → 1.0.0)
|
||||
type=semver,pattern={{version}}
|
||||
# Tag major.minor from git tags (v1.2.3 → 1.2)
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
# Tag major from git tags (v1.2.3 → 1)
|
||||
type=semver,pattern={{major}}
|
||||
# Ephemeral tag for pull requests (derive number from GITHUB_REF if available)
|
||||
type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }}
|
||||
# Short SHA tag as fallback (for non-default non-dev push events)
|
||||
type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}
|
||||
|
||||
# Step 6: Build the frontend first
|
||||
- name: 🎨 Build frontend
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
# Step 7: Build and push Docker image
|
||||
- name: 🐳 Build and push Docker image
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
CADDY_IMAGE=${{ steps.caddy.outputs.image }}
|
||||
|
||||
# Step 8: Run Trivy scan (table output first for visibility)
|
||||
- name: 📋 Run Trivy scan (table output)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy_table
|
||||
continue-on-error: true
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
|
||||
format: 'table'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
exit-code: '0'
|
||||
|
||||
# Step 9: Run Trivy security scan (SARIF)
|
||||
- name: 🔍 Run Trivy vulnerability scanner
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy
|
||||
continue-on-error: true
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
exit-code: '0'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
# Step 10: Upload Trivy results to GitHub Security tab
|
||||
- name: 📤 Upload Trivy results to GitHub Security
|
||||
uses: github/codeql-action/upload-sarif@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && (steps.trivy.outcome == 'success' || steps.trivy.outcome == 'failure')
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
# Step 11: Fail if vulnerabilities found
|
||||
- name: ❌ Check for vulnerabilities
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy_table.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error::CRITICAL or HIGH vulnerabilities found in image"
|
||||
exit 1
|
||||
|
||||
# Step 11: Create a summary
|
||||
- name: 📋 Create summary
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
run: |
|
||||
echo "## 🎉 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Image Details" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🚀 How to Use" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker run -d -p 8080:8080 -v caddy_data:/app/data ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: 📋 Create skip summary
|
||||
if: steps.skip.outputs.skip_build == 'true'
|
||||
run: |
|
||||
echo "## 🚫 Docker Build Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Reason: renovate bot or chore(deps)/chore commit/PR title." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Actor: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Event: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test-image:
|
||||
name: Test Docker Image
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.build-and-push.outputs.skip_build != 'true'
|
||||
|
||||
steps:
|
||||
# Ensure normalized lowercase IMAGE_NAME for this job as well
|
||||
- name: 🔤 Normalize image name
|
||||
run: |
|
||||
raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
|
||||
echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
# Step 1: Figure out which tag to test
|
||||
- name: 🏷️ Determine image tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then
|
||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Step 1.5: Log in to GitHub Container Registry (Required for private/internal images)
|
||||
- name: 🔐 Log in to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.PROJECT_TOKEN }}
|
||||
|
||||
# Step 2: Pull the image we just built
|
||||
- name: 📥 Pull Docker image
|
||||
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
|
||||
# Step 3: Start the container
|
||||
- name: 🚀 Run container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name test-container \
|
||||
-p 8080:8080 \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
|
||||
# Step 4/5: Wait and check health with retries
|
||||
- name: 🏥 Test health endpoint (retries)
|
||||
run: |
|
||||
set +e
|
||||
for i in $(seq 1 30); do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000")
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "✅ Health check passed on attempt $i"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/30: health not ready (code=$code); waiting..."
|
||||
sleep 2
|
||||
done
|
||||
echo "❌ Health check failed after retries"
|
||||
docker logs test-container || true
|
||||
exit 1
|
||||
|
||||
# Step 6: Check the logs for errors
|
||||
- name: 📋 Check container logs
|
||||
if: always()
|
||||
run: docker logs test-container
|
||||
|
||||
# Step 7: Clean up
|
||||
- name: 🧹 Stop container
|
||||
if: always()
|
||||
run: docker stop test-container && docker rm test-container
|
||||
|
||||
# Step 8: Summary
|
||||
- name: 📋 Create test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Health Check**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,134 +0,0 @@
|
||||
name: Docker Build & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/cpmp
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Determine skip condition
|
||||
id: skip
|
||||
env:
|
||||
ACTOR: ${{ github.actor }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
HEAD_MSG: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
should_skip=false
|
||||
pr_title=""
|
||||
if [ "$EVENT" = "pull_request" ]; then
|
||||
pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '')
|
||||
fi
|
||||
if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
echo "skip_build=$should_skip" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Resolve Caddy base digest
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: caddy
|
||||
run: |
|
||||
docker pull caddy:2-alpine
|
||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
|
||||
echo "image=$DIGEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to Container Registry
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.PROJECT_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: meta
|
||||
uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c # v5.0.1
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0
|
||||
with:
|
||||
context: .
|
||||
# PRs: amd64 only, no push. Pushes: amd64+arm64, push.
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
CADDY_IMAGE=${{ steps.caddy.outputs.image }}
|
||||
|
||||
# Trivy steps only on push
|
||||
- name: Run Trivy scan (table output)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||
format: 'table'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
exit-code: '0'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner (SARIF)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload Trivy results
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: github/codeql-action/upload-sarif@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
@@ -1,353 +0,0 @@
|
||||
name: Deploy Documentation to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Deploy docs when pushing to main
|
||||
paths:
|
||||
- 'docs/**' # Only run if docs folder changes
|
||||
- 'README.md' # Or if README changes
|
||||
- '.github/workflows/docs.yml' # Or if this workflow changes
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
# Sets permissions to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Documentation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Step 1: Get the code
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
# Step 2: Set up Node.js (for building any JS-based doc tools)
|
||||
- name: 🔧 Set up Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: '20.19.5'
|
||||
|
||||
# Step 3: Create a beautiful docs site structure
|
||||
- name: 📝 Build documentation site
|
||||
run: |
|
||||
# Create output directory
|
||||
mkdir -p _site
|
||||
|
||||
# Copy all markdown files
|
||||
cp README.md _site/
|
||||
cp -r docs _site/
|
||||
|
||||
# Create a simple HTML index that looks nice
|
||||
cat > _site/index.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Caddy Proxy Manager Plus - Documentation</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1d4ed8;
|
||||
--primary-hover: #1e40af;
|
||||
}
|
||||
body {
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
header {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 100%);
|
||||
padding: 3rem 0;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
header h1 {
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
header p {
|
||||
color: #e0e7ff;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.card h3 {
|
||||
color: #60a5fa;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card p {
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.card a {
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card a:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.badge-beginner {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
.badge-advanced {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 3rem 0;
|
||||
color: #64748b;
|
||||
border-top: 1px solid #334155;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🚀 Caddy Proxy Manager Plus</h1>
|
||||
<p>Make your websites easy to reach - No coding required!</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<section>
|
||||
<h2>👋 Welcome!</h2>
|
||||
<p style="font-size: 1.1rem; color: #cbd5e1;">
|
||||
This documentation will help you get started with Caddy Proxy Manager Plus.
|
||||
Whether you're a complete beginner or an experienced developer, we've got you covered!
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<h2 style="margin-top: 3rem;">📚 Getting Started</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>🏠 Getting Started Guide <span class="badge badge-beginner">Start Here</span></h3>
|
||||
<p>Your first setup in just 5 minutes! We'll walk you through everything step by step.</p>
|
||||
<a href="docs/getting-started.html">Read the Guide →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📖 README <span class="badge badge-beginner">Essential</span></h3>
|
||||
<p>Learn what the app does, how to install it, and see examples of what you can build.</p>
|
||||
<a href="README.html">Read More →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📥 Import Guide</h3>
|
||||
<p>Already using Caddy? Learn how to bring your existing configuration into the app.</p>
|
||||
<a href="docs/import-guide.html">Import Your Configs →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 3rem;">🔧 Developer Documentation</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>🔌 API Reference <span class="badge badge-advanced">Advanced</span></h3>
|
||||
<p>Complete REST API documentation with examples in JavaScript and Python.</p>
|
||||
<a href="docs/api.html">View API Docs →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>💾 Database Schema <span class="badge badge-advanced">Advanced</span></h3>
|
||||
<p>Understand how data is stored, relationships, and backup strategies.</p>
|
||||
<a href="docs/database-schema.html">View Schema →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>✨ Contributing Guide</h3>
|
||||
<p>Want to help make this better? Learn how to contribute code, docs, or ideas.</p>
|
||||
<a href="CONTRIBUTING.html">Start Contributing →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 3rem;">📋 All Documentation</h2>
|
||||
<div class="card">
|
||||
<h3>📚 Documentation Index</h3>
|
||||
<p>Browse all available documentation organized by topic and skill level.</p>
|
||||
<a href="docs/index.html">View Full Index →</a>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 3rem;">🆘 Need Help?</h2>
|
||||
<div class="card" style="background: #1e3a8a; border-color: #1e40af;">
|
||||
<h3 style="color: #dbeafe;">Get Support</h3>
|
||||
<p style="color: #bfdbfe;">
|
||||
Stuck? Have questions? We're here to help!
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1rem;">
|
||||
<a href="https://github.com/Wikid82/CaddyProxyManagerPlus/discussions"
|
||||
style="background: white; color: #1e40af; padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">
|
||||
💬 Ask a Question
|
||||
</a>
|
||||
<a href="https://github.com/Wikid82/CaddyProxyManagerPlus/issues"
|
||||
style="background: white; color: #1e40af; padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">
|
||||
🐛 Report a Bug
|
||||
</a>
|
||||
<a href="https://github.com/Wikid82/CaddyProxyManagerPlus"
|
||||
style="background: white; color: #1e40af; padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">
|
||||
⭐ View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Built with ❤️ by <a href="https://github.com/Wikid82" style="color: #60a5fa;">@Wikid82</a></p>
|
||||
<p>Made for humans, not just techies!</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
# Convert markdown files to HTML using a simple converter
|
||||
npm install -g marked
|
||||
|
||||
# Convert each markdown file
|
||||
for file in _site/docs/*.md; do
|
||||
if [ -f "$file" ]; then
|
||||
filename=$(basename "$file" .md)
|
||||
marked "$file" -o "_site/docs/${filename}.html" --gfm
|
||||
fi
|
||||
done
|
||||
|
||||
# Convert README and CONTRIBUTING
|
||||
marked _site/README.md -o _site/README.html --gfm
|
||||
if [ -f "CONTRIBUTING.md" ]; then
|
||||
cp CONTRIBUTING.md _site/
|
||||
marked _site/CONTRIBUTING.md -o _site/CONTRIBUTING.html --gfm
|
||||
fi
|
||||
|
||||
# Add simple styling to all HTML files
|
||||
for html_file in _site/*.html _site/docs/*.html; do
|
||||
if [ -f "$html_file" ] && [ "$html_file" != "_site/index.html" ]; then
|
||||
# Add a header with navigation to each page
|
||||
temp_file="${html_file}.tmp"
|
||||
cat > "$temp_file" << 'HEADER'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Caddy Proxy Manager Plus - Documentation</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<style>
|
||||
body { background-color: #0f172a; color: #e2e8f0; }
|
||||
nav { background: #1e293b; padding: 1rem; margin-bottom: 2rem; }
|
||||
nav a { color: #60a5fa; margin-right: 1rem; text-decoration: none; }
|
||||
nav a:hover { color: #93c5fd; }
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem; }
|
||||
a { color: #60a5fa; }
|
||||
code { background: #1e293b; color: #fbbf24; padding: 0.2rem 0.4rem; border-radius: 4px; }
|
||||
pre { background: #1e293b; padding: 1rem; border-radius: 8px; overflow-x: auto; }
|
||||
pre code { background: none; padding: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">🏠 Home</a>
|
||||
<a href="/docs/index.html">📚 Docs</a>
|
||||
<a href="/docs/getting-started.html">🚀 Get Started</a>
|
||||
<a href="https://github.com/Wikid82/CaddyProxyManagerPlus">⭐ GitHub</a>
|
||||
</nav>
|
||||
<main>
|
||||
HEADER
|
||||
|
||||
# Append original content
|
||||
cat "$html_file" >> "$temp_file"
|
||||
|
||||
# Add footer
|
||||
cat >> "$temp_file" << 'FOOTER'
|
||||
</main>
|
||||
<footer style="text-align: center; padding: 2rem; color: #64748b;">
|
||||
<p>Caddy Proxy Manager Plus - Built with ❤️ for the community</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
FOOTER
|
||||
|
||||
mv "$temp_file" "$html_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Documentation site built successfully!"
|
||||
|
||||
# Step 4: Upload the built site
|
||||
- name: 📤 Upload artifact
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
with:
|
||||
path: '_site'
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
# Deploy to GitHub Pages
|
||||
- name: 🚀 Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
|
||||
# Create a summary
|
||||
- name: 📋 Create deployment summary
|
||||
run: |
|
||||
echo "## 🎉 Documentation Deployed!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Your documentation is now live at:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🔗 ${{ steps.deployment.outputs.page_url }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📚 What's Included" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Getting Started Guide" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Complete README" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- API Documentation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Database Schema" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Import Guide" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Contributing Guidelines" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Propagate Changes Between Branches
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
propagate:
|
||||
name: Create PR to synchronize branches
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'github-actions[bot]' && github.event.pusher != null
|
||||
steps:
|
||||
- name: Set up Node (for github-script)
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: '20.19.5'
|
||||
|
||||
- name: Propagate Changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const currentBranch = context.ref.replace('refs/heads/', '');
|
||||
|
||||
async function createPR(src, base) {
|
||||
if (src === base) return;
|
||||
|
||||
core.info(`Checking propagation from ${src} to ${base}...`);
|
||||
|
||||
// Check for existing open PRs
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${src}`,
|
||||
base: base,
|
||||
});
|
||||
|
||||
if (pulls.length > 0) {
|
||||
core.info(`Existing PR found for ${src} -> ${base}. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare commits to see if src is ahead of base
|
||||
try {
|
||||
const compare = await github.rest.repos.compareCommits({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: base,
|
||||
head: src,
|
||||
});
|
||||
|
||||
// If src is not ahead, nothing to merge
|
||||
if (compare.data.ahead_by === 0) {
|
||||
core.info(`${src} is not ahead of ${base}. No propagation needed.`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// If base branch doesn't exist, etc.
|
||||
core.warning(`Error comparing ${src} to ${base}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PR
|
||||
try {
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `Propagate changes from ${src} into ${base}`,
|
||||
head: src,
|
||||
base: base,
|
||||
body: `Automated PR to propagate changes from ${src} into ${base}.\n\nTriggered by push to ${currentBranch}.`,
|
||||
});
|
||||
core.info(`Created PR #${pr.data.number} to merge ${src} into ${base}`);
|
||||
} catch (error) {
|
||||
core.warning(`Failed to create PR from ${src} to ${base}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBranch === 'main') {
|
||||
// Main -> Development
|
||||
await createPR('main', 'development');
|
||||
} else if (currentBranch === 'development') {
|
||||
// Development -> Feature branches
|
||||
const branches = await github.paginate(github.rest.repos.listBranches, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const featureBranches = branches
|
||||
.map(b => b.name)
|
||||
.filter(name => name.startsWith('feature/'));
|
||||
|
||||
core.info(`Found ${featureBranches.length} feature branches: ${featureBranches.join(', ')}`);
|
||||
|
||||
for (const featureBranch of featureBranches) {
|
||||
await createPR('development', featureBranch);
|
||||
}
|
||||
}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Quality Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, development, 'feature/**' ]
|
||||
pull_request:
|
||||
branches: [ main, development ]
|
||||
|
||||
jobs:
|
||||
backend-quality:
|
||||
name: Backend (Go)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: '1.24.4'
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Run Go tests
|
||||
working-directory: backend
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@d6238b002a20823d52840fda27e2d4891c5952dc # v4.0.1
|
||||
with:
|
||||
version: latest
|
||||
working-directory: backend
|
||||
args: --timeout=5m
|
||||
continue-on-error: true
|
||||
|
||||
frontend-quality:
|
||||
name: Frontend (React)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: '20.19.5'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Run frontend tests
|
||||
working-directory: frontend
|
||||
run: npm test
|
||||
|
||||
- name: Run frontend lint
|
||||
working-directory: frontend
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# Get previous tag
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "First release - generating full changelog"
|
||||
CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges)
|
||||
else
|
||||
echo "Generating changelog since $PREV_TAG"
|
||||
CHANGELOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges)
|
||||
fi
|
||||
|
||||
# Save to file for GitHub release
|
||||
echo "$CHANGELOG" > CHANGELOG.txt
|
||||
echo "Generated changelog with $(echo "$CHANGELOG" | wc -l) commits"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
body_path: CHANGELOG.txt
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
||||
|
||||
build-and-publish:
|
||||
needs: create-release
|
||||
uses: ./.github/workflows/docker-publish.yml
|
||||
secrets: inherit
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *' # daily 05:00 EST
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Run Renovate
|
||||
uses: renovatebot/github-action@0984fb80fc633b17e57f3e8b6c007fe0dc3e0d62 # v40.3.6
|
||||
with:
|
||||
configurationFile: .github/renovate.json
|
||||
token: ${{ secrets.PROJECT_TOKEN }}
|
||||
env:
|
||||
LOG_LEVEL: info
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
*.cover
|
||||
.hypothesis/
|
||||
htmlcov/
|
||||
|
||||
# Node/Frontend
|
||||
frontend/node_modules/
|
||||
backend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/coverage/
|
||||
frontend/.vite/
|
||||
frontend/*.tsbuildinfo
|
||||
|
||||
# Go/Backend
|
||||
backend/api
|
||||
backend/*.out
|
||||
backend/coverage/
|
||||
backend/coverage.*.out
|
||||
|
||||
# Databases
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
backend/data/*.db
|
||||
backend/cmd/api/data/*.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
|
||||
# Caddy
|
||||
backend/data/caddy/
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
*.xml
|
||||
@@ -1,27 +0,0 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: python-compile
|
||||
name: python compile check
|
||||
entry: tools/python_compile_check.sh
|
||||
language: script
|
||||
files: ".*\\.py$"
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)'
|
||||
- id: trailing-whitespace
|
||||
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)'
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: dockerfile-check
|
||||
name: dockerfile validation
|
||||
entry: tools/dockerfile_check.sh
|
||||
language: script
|
||||
files: "Dockerfile.*"
|
||||
pass_filenames: true
|
||||
@@ -1,4 +0,0 @@
|
||||
version: 1
|
||||
exclude:
|
||||
- frontend/dist/**
|
||||
- frontend/node_modules/**
|
||||
@@ -1,49 +0,0 @@
|
||||
# CaddyProxyManager+ Architecture Plan
|
||||
|
||||
## Stack Overview
|
||||
- **Backend**: Go 1.24, Gin HTTP framework, GORM ORM, SQLite for local/stateful storage.
|
||||
- **Frontend**: React 18 + TypeScript with Vite, React Query for data fetching, React Router for navigation.
|
||||
- **API Contract**: REST/JSON over `/api/v1`, versioned to keep room for breaking changes.
|
||||
- **Deployment**: Container-first via multi-stage Docker build (Node → Go), future compose bundle for Caddy runtime.
|
||||
|
||||
## Backend
|
||||
- `backend/cmd/api`: Entry point wires configuration, database, and HTTP server lifecycle.
|
||||
- `internal/config`: Reads environment variables (`CPM_ENV`, `CPM_HTTP_PORT`, `CPM_DB_PATH`). Defaults to `development`, `8080`, `./data/cpm.db` respectively.
|
||||
- `internal/database`: Wraps GORM + SQLite connection handling and enforces data-directory creation.
|
||||
- `internal/server`: Creates Gin engine, registers middleware, wires graceful shutdown, and exposes `Run(ctx)` for signal-aware lifecycle.
|
||||
- `internal/api`: Versioned routing layer. Initial resources:
|
||||
- `GET /api/v1/health`: Simple status response for readiness checks.
|
||||
- CRUD `/api/v1/proxy-hosts`: Minimal data model used to validate persistence, shape matches Issue #1 requirements (name, domain, upstream target, toggles).
|
||||
- `internal/models`: Source of truth for persistent entities. Future migrations will extend `ProxyHost` with SSL, ACL, audit metadata.
|
||||
- Testing: In-memory SQLite harness verifies handler lifecycle via unit tests (`go test ./...`).
|
||||
|
||||
## Frontend
|
||||
- Vite dev server with proxy to `http://localhost:8080` for `/api` paths keeps CORS trivial.
|
||||
- React Router organizes initial pages (Dashboard, Proxy Hosts, System Status) to mirror Issue roadmap.
|
||||
- React Query centralizes API caching, invalidation, and loading states.
|
||||
- Basic layout shell provides left-nav reminiscent of NPM while keeping styling simple (CSS utility file, no design system yet). Future work will slot shadcn/ui components without rewriting data layer.
|
||||
- Build outputs static assets in `frontend/dist` consumed by Docker multi-stage for production.
|
||||
|
||||
## Data & Persistence
|
||||
- SQLite chosen for Alpha milestone simplicity; GORM migrates schema automatically on boot (`AutoMigrate`).
|
||||
- Database path configurable via env to allow persistent volumes in Docker or alternative DB (PostgreSQL/MySQL) when scaling.
|
||||
|
||||
## API Principles
|
||||
1. **Version Everything** (`/api/v1`).
|
||||
2. **Stateless**: Each request carries all context; session/story features will rely on cookies/JWT later.
|
||||
3. **Dependable validation**: Gin binding ensures HTTP 400 responses include validation errors.
|
||||
4. **Observability**: Gin logging + structured error responses keep early debugging simple; plan to add Zap/zerolog instrumentation during Beta.
|
||||
|
||||
## Local Development Workflow
|
||||
1. Start backend: `cd backend && go run ./cmd/api`.
|
||||
2. Start frontend: `cd frontend && npm run dev` (Vite proxy sends API calls to backend automatically).
|
||||
3. Optional: run both via Docker (see updated Dockerfile) once containers land.
|
||||
4. Tests:
|
||||
- Backend: `cd backend && go test ./...`
|
||||
- Frontend build check: `cd frontend && npm run build`
|
||||
|
||||
## Next Steps
|
||||
- Layer authentication (Issue #7) once scaffolding lands.
|
||||
- Expand data model (certificates, access lists) and add migrations.
|
||||
- Replace basic CSS with component system (e.g., shadcn/ui) + design tokens.
|
||||
- Compose file bundling backend, frontend assets, Caddy runtime, and SQLite volume.
|
||||
-387
@@ -1,387 +0,0 @@
|
||||
# Contributing to CaddyProxyManager+
|
||||
|
||||
Thank you for your interest in contributing to CaddyProxyManager+! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Coding Standards](#coding-standards)
|
||||
- [Testing Guidelines](#testing-guidelines)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Issue Guidelines](#issue-guidelines)
|
||||
- [Documentation](#documentation)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows a Code of Conduct that all contributors are expected to adhere to:
|
||||
|
||||
- Be respectful and inclusive
|
||||
- Welcome newcomers and help them get started
|
||||
- Focus on what's best for the community
|
||||
- Show empathy towards other community members
|
||||
|
||||
## Getting Started
|
||||
|
||||
-### Prerequisites
|
||||
|
||||
- **Go 1.24+** for backend development
|
||||
- **Node.js 20+** and npm for frontend development
|
||||
- Git for version control
|
||||
- A GitHub account
|
||||
|
||||
### Fork and Clone
|
||||
|
||||
1. Fork the repository on GitHub
|
||||
2. Clone your fork locally:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/CaddyProxyManagerPlus.git
|
||||
cd CaddyProxyManagerPlus
|
||||
```
|
||||
|
||||
3. Add the upstream remote:
|
||||
```bash
|
||||
git remote add upstream https://github.com/Wikid82/CaddyProxyManagerPlus.git
|
||||
```
|
||||
|
||||
### Set Up Development Environment
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
cd backend
|
||||
go mod download
|
||||
go run ./cmd/seed/main.go # Seed test data
|
||||
go run ./cmd/api/main.go # Start backend
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # Start frontend dev server
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
- **main** - Production-ready code
|
||||
- **development** - Main development branch (default)
|
||||
- **feature/** - Feature branches (e.g., `feature/add-ssl-support`)
|
||||
- **bugfix/** - Bug fix branches (e.g., `bugfix/fix-import-crash`)
|
||||
- **hotfix/** - Urgent production fixes
|
||||
|
||||
### Creating a Feature Branch
|
||||
|
||||
Always branch from `development`:
|
||||
|
||||
```bash
|
||||
git checkout development
|
||||
git pull upstream development
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
### Commit Message Guidelines
|
||||
|
||||
Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation only
|
||||
- `style`: Code style changes (formatting, etc.)
|
||||
- `refactor`: Code refactoring
|
||||
- `test`: Adding or updating tests
|
||||
- `chore`: Maintenance tasks
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat(proxy-hosts): add SSL certificate upload
|
||||
|
||||
- Implement certificate upload endpoint
|
||||
- Add UI for certificate management
|
||||
- Update database schema
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
```
|
||||
fix(import): resolve conflict detection bug
|
||||
|
||||
When importing Caddyfiles with multiple domains, conflicts
|
||||
were not being detected properly.
|
||||
|
||||
Fixes #456
|
||||
```
|
||||
|
||||
### Keeping Your Fork Updated
|
||||
|
||||
```bash
|
||||
git checkout development
|
||||
git fetch upstream
|
||||
git merge upstream/development
|
||||
git push origin development
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Go Backend
|
||||
|
||||
- Follow standard Go formatting (`gofmt`)
|
||||
- Use meaningful variable and function names
|
||||
- Write godoc comments for exported functions
|
||||
- Keep functions small and focused
|
||||
- Handle errors explicitly
|
||||
|
||||
**Example:**
|
||||
```go
|
||||
// GetProxyHost retrieves a proxy host by UUID.
|
||||
// Returns an error if the host is not found.
|
||||
func GetProxyHost(uuid string) (*models.ProxyHost, error) {
|
||||
var host models.ProxyHost
|
||||
if err := db.First(&host, "uuid = ?", uuid).Error; err != nil {
|
||||
return nil, fmt.Errorf("proxy host not found: %w", err)
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript Frontend
|
||||
|
||||
- Use TypeScript for type safety
|
||||
- Follow React best practices and hooks patterns
|
||||
- Use functional components
|
||||
- Destructure props at function signature
|
||||
- Extract reusable logic into custom hooks
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
interface ProxyHostFormProps {
|
||||
host?: ProxyHost
|
||||
onSubmit: (data: ProxyHostData) => Promise<void>
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) {
|
||||
const [domain, setDomain] = useState(host?.domain ?? '')
|
||||
// ... component logic
|
||||
}
|
||||
```
|
||||
|
||||
### CSS/Styling
|
||||
|
||||
- Use TailwindCSS utility classes
|
||||
- Follow the dark theme color palette
|
||||
- Keep custom CSS minimal
|
||||
- Use semantic color names from the theme
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Backend Tests
|
||||
|
||||
Write tests for all new functionality:
|
||||
|
||||
```go
|
||||
func TestGetProxyHost(t *testing.T) {
|
||||
// Setup
|
||||
db := setupTestDB(t)
|
||||
host := createTestHost(db)
|
||||
|
||||
// Execute
|
||||
result, err := GetProxyHost(host.UUID)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, host.Domain, result.Domain)
|
||||
}
|
||||
```
|
||||
|
||||
**Run tests:**
|
||||
```bash
|
||||
go test ./... -v
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
Write component and hook tests using Vitest and React Testing Library:
|
||||
|
||||
```typescript
|
||||
describe('ProxyHostForm', () => {
|
||||
it('renders create form with empty fields', async () => {
|
||||
render(
|
||||
<ProxyHostForm onSubmit={vi.fn()} onCancel={vi.fn()} />
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Add Proxy Host')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Run tests:**
|
||||
```bash
|
||||
npm test # Watch mode
|
||||
npm run test:coverage # Coverage report
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Aim for 80%+ code coverage
|
||||
- All new features must include tests
|
||||
- Bug fixes should include regression tests
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Submitting
|
||||
|
||||
1. **Ensure tests pass:**
|
||||
```bash
|
||||
# Backend
|
||||
go test ./...
|
||||
|
||||
# Frontend
|
||||
npm test -- --run
|
||||
```
|
||||
|
||||
2. **Check code quality:**
|
||||
```bash
|
||||
# Go formatting
|
||||
go fmt ./...
|
||||
|
||||
# Frontend linting
|
||||
npm run lint
|
||||
```
|
||||
|
||||
3. **Update documentation** if needed
|
||||
4. **Add tests** for new functionality
|
||||
5. **Rebase on latest development** branch
|
||||
|
||||
### Submitting a Pull Request
|
||||
|
||||
1. Push your branch to your fork:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
2. Open a Pull Request on GitHub
|
||||
3. Fill out the PR template completely
|
||||
4. Link related issues using "Closes #123" or "Fixes #456"
|
||||
5. Request review from maintainers
|
||||
|
||||
### PR Template
|
||||
|
||||
```markdown
|
||||
## Description
|
||||
Brief description of changes
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation update
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Manual testing performed
|
||||
- [ ] All tests passing
|
||||
|
||||
## Screenshots (if applicable)
|
||||
Add screenshots of UI changes
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows style guidelines
|
||||
- [ ] Self-review performed
|
||||
- [ ] Comments added for complex code
|
||||
- [ ] Documentation updated
|
||||
- [ ] No new warnings generated
|
||||
```
|
||||
|
||||
### Review Process
|
||||
|
||||
- Maintainers will review within 2-3 business days
|
||||
- Address review feedback promptly
|
||||
- Keep discussions focused and professional
|
||||
- Be open to suggestions and alternative approaches
|
||||
|
||||
## Issue Guidelines
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
Use the bug report template and include:
|
||||
|
||||
- Clear, descriptive title
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- Environment details (OS, browser, Go version, etc.)
|
||||
- Screenshots or error logs
|
||||
- Potential solutions (if known)
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Use the feature request template and include:
|
||||
|
||||
- Clear description of the feature
|
||||
- Use case and motivation
|
||||
- Potential implementation approach
|
||||
- Mockups or examples (if applicable)
|
||||
|
||||
### Issue Labels
|
||||
|
||||
- `bug` - Something isn't working
|
||||
- `enhancement` - New feature or request
|
||||
- `documentation` - Documentation improvements
|
||||
- `good first issue` - Good for newcomers
|
||||
- `help wanted` - Extra attention needed
|
||||
- `priority: high` - Urgent issue
|
||||
- `wontfix` - Will not be fixed
|
||||
|
||||
## Documentation
|
||||
|
||||
### Code Documentation
|
||||
|
||||
- Add docstrings to all exported functions
|
||||
- Include examples in complex functions
|
||||
- Document return types and error conditions
|
||||
- Keep comments up-to-date with code changes
|
||||
|
||||
### Project Documentation
|
||||
|
||||
When adding features, update:
|
||||
|
||||
- `README.md` - User-facing information
|
||||
- `docs/api.md` - API changes
|
||||
- `docs/import-guide.md` - Import feature updates
|
||||
- `docs/database-schema.md` - Schema changes
|
||||
|
||||
## Recognition
|
||||
|
||||
Contributors will be recognized in:
|
||||
|
||||
- CONTRIBUTORS.md file
|
||||
- Release notes for significant contributions
|
||||
- GitHub contributors page
|
||||
|
||||
## Questions?
|
||||
|
||||
- Open a [Discussion](https://github.com/Wikid82/CaddyProxyManagerPlus/discussions) for general questions
|
||||
- Join our community chat (coming soon)
|
||||
- Tag maintainers in issues for urgent matters
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the project's MIT License.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to CaddyProxyManager+! 🎉
|
||||
@@ -1,234 +0,0 @@
|
||||
# Docker Deployment Guide
|
||||
|
||||
CaddyProxyManager+ is designed for Docker-first deployment, making it easy for home users to run Caddy without learning Caddyfile syntax.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Wikid82/CaddyProxyManagerPlus.git
|
||||
cd CaddyProxyManagerPlus
|
||||
|
||||
# Start the stack
|
||||
docker-compose up -d
|
||||
|
||||
# Access the UI
|
||||
open http://localhost:8080
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Docker stack consists of two services:
|
||||
|
||||
1. **app** (`caddyproxymanager-plus`): Management interface
|
||||
- Manages proxy host configuration
|
||||
- Provides web UI on port 8080
|
||||
- Communicates with Caddy via admin API
|
||||
|
||||
2. **caddy**: Reverse proxy server
|
||||
- Handles incoming traffic on ports 80/443
|
||||
- Automatic HTTPS with Let's Encrypt
|
||||
- Configured dynamically via JSON API
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ Internet │
|
||||
└──────┬───────┘
|
||||
│ :80, :443
|
||||
▼
|
||||
┌──────────────┐ Admin API ┌──────────────┐
|
||||
│ Caddy │◄───────:2019───────┤ CPM+ App │
|
||||
│ (Proxy) │ │ (Manager) │
|
||||
└──────┬───────┘ └──────┬───────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
Your Services :8080 (Web UI)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Configure CPM+ via environment variables in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- CPM_ENV=production # production | development
|
||||
- CPM_HTTP_PORT=8080 # Management UI port
|
||||
- CPM_DB_PATH=/app/data/cpm.db # SQLite database location
|
||||
- CPM_CADDY_ADMIN_API=http://caddy:2019 # Caddy admin endpoint
|
||||
- CPM_CADDY_CONFIG_DIR=/app/data/caddy # Config snapshots
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
Three persistent volumes store your data:
|
||||
|
||||
- **app_data**: CPM+ database, config snapshots, logs
|
||||
- **caddy_data**: Caddy certificates, ACME account data
|
||||
- **caddy_config**: Caddy runtime configuration
|
||||
|
||||
To backup your configuration:
|
||||
|
||||
```bash
|
||||
# Backup volumes
|
||||
docker run --rm -v cpm_app_data:/data -v $(pwd):/backup alpine tar czf /backup/cpm-backup.tar.gz /data
|
||||
|
||||
# Restore from backup
|
||||
docker run --rm -v cpm_app_data:/data -v $(pwd):/backup alpine tar xzf /backup/cpm-backup.tar.gz -C /
|
||||
```
|
||||
|
||||
## Ports
|
||||
|
||||
Default port mapping:
|
||||
|
||||
- **80**: HTTP (Caddy) - redirects to HTTPS
|
||||
- **443/tcp**: HTTPS (Caddy)
|
||||
- **443/udp**: HTTP/3 (Caddy)
|
||||
- **8080**: Management UI (CPM+)
|
||||
- **2019**: Caddy admin API (internal only, exposed in dev mode)
|
||||
|
||||
## Development Mode
|
||||
|
||||
Development mode exposes the Caddy admin API externally for debugging:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
Access Caddy admin API: `http://localhost:2019/config/`
|
||||
|
||||
## Health Checks
|
||||
|
||||
CPM+ includes a health check endpoint:
|
||||
|
||||
```bash
|
||||
# Check if app is running
|
||||
curl http://localhost:8080/api/v1/health
|
||||
|
||||
# Check Caddy status
|
||||
docker-compose exec caddy caddy version
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### App can't reach Caddy
|
||||
|
||||
**Symptom**: "Caddy unreachable" errors in logs
|
||||
|
||||
**Solution**: Ensure both containers are on the same network:
|
||||
```bash
|
||||
docker-compose ps # Check both services are "Up"
|
||||
docker-compose logs caddy # Check Caddy logs
|
||||
```
|
||||
|
||||
### Certificates not working
|
||||
|
||||
**Symptom**: HTTP works but HTTPS fails
|
||||
|
||||
**Check**:
|
||||
1. Port 80/443 are accessible from the internet
|
||||
2. DNS points to your server
|
||||
3. Caddy logs: `docker-compose logs caddy | grep -i acme`
|
||||
|
||||
### Config changes not applied
|
||||
|
||||
**Symptom**: Changes in UI don't affect routing
|
||||
|
||||
**Debug**:
|
||||
```bash
|
||||
# View current Caddy config
|
||||
curl http://localhost:2019/config/ | jq
|
||||
|
||||
# Check CPM+ logs
|
||||
docker-compose logs app
|
||||
|
||||
# Manual config reload
|
||||
curl -X POST http://localhost:8080/api/v1/caddy/reload
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
Pull the latest images and restart:
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
For specific versions:
|
||||
|
||||
```bash
|
||||
# Edit docker-compose.yml to pin version
|
||||
image: ghcr.io/wikid82/caddyproxymanagerplus:v1.0.0
|
||||
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Build multi-arch images
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t caddyproxymanager-plus:local .
|
||||
|
||||
# Or use Make
|
||||
make docker-build
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Caddy admin API**: Keep port 2019 internal (not exposed in production compose)
|
||||
2. **Management UI**: Add authentication (Issue #7) before exposing to internet
|
||||
3. **Certificates**: Caddy stores private keys in `caddy_data` - protect this volume
|
||||
4. **Database**: SQLite file contains all config - backup regularly
|
||||
|
||||
## Integration with Existing Caddy
|
||||
|
||||
If you already have Caddy running, you can point CPM+ to it:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- CPM_CADDY_ADMIN_API=http://your-caddy-host:2019
|
||||
```
|
||||
|
||||
**Warning**: CPM+ will replace Caddy's entire configuration. Backup first!
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### Synology NAS
|
||||
|
||||
Use Container Manager (Docker GUI):
|
||||
1. Import `docker-compose.yml`
|
||||
2. Map port 80/443 to your NAS IP
|
||||
3. Enable auto-restart
|
||||
|
||||
### Unraid
|
||||
|
||||
1. Use Docker Compose Manager plugin
|
||||
2. Add compose file to `/boot/config/plugins/compose.manager/projects/cpm/`
|
||||
3. Start via web UI
|
||||
|
||||
### Home Assistant Add-on
|
||||
|
||||
Coming soon in Beta release.
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
For high-traffic deployments:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
caddy:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Configure your first proxy host via UI
|
||||
- Enable automatic HTTPS (happens automatically)
|
||||
- Add authentication (Issue #7)
|
||||
- Integrate CrowdSec (Issue #15)
|
||||
@@ -1,364 +0,0 @@
|
||||
# Documentation & CI/CD Polish Summary
|
||||
|
||||
## 🎯 Objectives Completed
|
||||
|
||||
This phase focused on making the project accessible to novice users and automating deployment processes:
|
||||
|
||||
1. ✅ Created comprehensive documentation index
|
||||
2. ✅ Rewrote all docs in beginner-friendly "ELI5" language
|
||||
3. ✅ Set up Docker CI/CD for multi-branch and version releases
|
||||
4. ✅ Configured GitHub Pages deployment for documentation
|
||||
5. ✅ Created setup guides for maintainers
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Improvements
|
||||
|
||||
### New Documentation Files Created
|
||||
|
||||
#### 1. **docs/index.md** (Homepage)
|
||||
- Central navigation hub for all documentation
|
||||
- Organized by user skill level (beginner vs. advanced)
|
||||
- Quick troubleshooting section
|
||||
- Links to all guides and references
|
||||
- Emoji-rich for easy scanning
|
||||
|
||||
#### 2. **docs/getting-started.md** (Beginner Guide)
|
||||
- Step-by-step first-time setup
|
||||
- Explains technical concepts with simple analogies
|
||||
- "What's a Proxy Host?" section with real examples
|
||||
- Drag-and-drop instructions
|
||||
- Common pitfalls and solutions
|
||||
- Encouragement for new users
|
||||
|
||||
#### 3. **docs/github-setup.md** (Maintainer Guide)
|
||||
- How to configure GitHub secrets for Docker Hub
|
||||
- Enabling GitHub Pages step-by-step
|
||||
- Testing workflows
|
||||
- Creating version releases
|
||||
- Troubleshooting common issues
|
||||
- Quick reference commands
|
||||
|
||||
### Updated Documentation Files
|
||||
|
||||
#### **README.md** - Complete Rewrite
|
||||
**Before**: Technical language with industry jargon
|
||||
**After**: Beginner-friendly explanations
|
||||
|
||||
Key Changes:
|
||||
- "Reverse proxy" → "Traffic director for your websites"
|
||||
- Technical architecture → "The brain and the face" analogy
|
||||
- Prerequisites → "What you need" with explanations
|
||||
- Commands explained with what they do
|
||||
- Added "Super Easy Way" (Docker one-liner)
|
||||
- Removed confusing terms, added plain English
|
||||
|
||||
**Example Before:**
|
||||
> "A modern, user-friendly web interface for managing Caddy reverse proxy configurations"
|
||||
|
||||
**Example After:**
|
||||
> "Make your websites easy to reach! Think of it like a traffic controller for your internet services"
|
||||
|
||||
**Simplification Examples:**
|
||||
- "SQLite Database" → "A tiny database (like a filing cabinet)"
|
||||
- "API endpoints" → "Commands you can send (like a robot that does work)"
|
||||
- "GORM ORM" → Removed technical acronym, explained purpose
|
||||
- "Component coverage" → "What's tested (proves it works!)"
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker CI/CD Workflow
|
||||
|
||||
### File: `.github/workflows/docker-build.yml`
|
||||
|
||||
**Triggers:**
|
||||
- Push to `main` → Creates `latest` tag
|
||||
- Push to `development` → Creates `dev` tag
|
||||
- Git tags like `v1.0.0` → Creates version tags (`1.0.0`, `1.0`, `1`)
|
||||
- Manual trigger via GitHub UI
|
||||
|
||||
**Features:**
|
||||
1. **Multi-Platform Builds**
|
||||
- Supports AMD64 and ARM64 architectures
|
||||
- Uses QEMU for cross-compilation
|
||||
- Build cache for faster builds
|
||||
|
||||
2. **Automatic Tagging**
|
||||
- Semantic versioning support
|
||||
- Git SHA tagging for traceability
|
||||
- Branch-specific tags
|
||||
|
||||
3. **Automated Testing**
|
||||
- Pulls the built image
|
||||
- Starts container
|
||||
- Tests health endpoint
|
||||
- Displays logs on failure
|
||||
|
||||
4. **User-Friendly Output**
|
||||
- Rich summaries with emojis
|
||||
- Pull commands for users
|
||||
- Test results displayed clearly
|
||||
|
||||
**Tags Generated:**
|
||||
```
|
||||
main branch:
|
||||
- latest
|
||||
- sha-abc1234
|
||||
|
||||
development branch:
|
||||
- dev
|
||||
- sha-abc1234
|
||||
|
||||
v1.2.3 tag:
|
||||
- 1.2.3
|
||||
- 1.2
|
||||
- 1
|
||||
- sha-abc1234
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 GitHub Pages Workflow
|
||||
|
||||
### File: `.github/workflows/docs.yml`
|
||||
|
||||
**Triggers:**
|
||||
- Changes to `docs/` folder
|
||||
- Changes to `README.md`
|
||||
- Manual trigger via GitHub UI
|
||||
|
||||
**Features:**
|
||||
1. **Beautiful Landing Page**
|
||||
- Custom HTML homepage with dark theme
|
||||
- Card-based navigation
|
||||
- Skill level badges (Beginner/Advanced)
|
||||
- Responsive design
|
||||
- Matches app's dark blue theme (#0f172a)
|
||||
|
||||
2. **Markdown to HTML Conversion**
|
||||
- Uses `marked` for GitHub-flavored markdown
|
||||
- Adds navigation header to every page
|
||||
- Consistent styling across all pages
|
||||
- Code syntax highlighting
|
||||
|
||||
3. **Professional Styling**
|
||||
- Dark theme (#0f172a background)
|
||||
- Blue accents (#1d4ed8)
|
||||
- Hover effects on cards
|
||||
- Mobile-responsive layout
|
||||
- Uses Pico CSS for base styling
|
||||
|
||||
4. **Automatic Deployment**
|
||||
- Builds on every docs change
|
||||
- Deploys to GitHub Pages
|
||||
- Provides published URL
|
||||
- Summary with included files
|
||||
|
||||
**Published Site Structure:**
|
||||
```
|
||||
https://wikid82.github.io/CaddyProxyManagerPlus/
|
||||
├── index.html (custom homepage)
|
||||
├── README.html
|
||||
├── CONTRIBUTING.html
|
||||
└── docs/
|
||||
├── index.html
|
||||
├── getting-started.html
|
||||
├── api.html
|
||||
├── database-schema.html
|
||||
├── import-guide.html
|
||||
└── github-setup.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Design Philosophy
|
||||
|
||||
### "Explain Like I'm 5" Approach
|
||||
|
||||
**Principles Applied:**
|
||||
1. **Use Analogies** - Complex concepts explained with familiar examples
|
||||
2. **Avoid Jargon** - Technical terms replaced or explained
|
||||
3. **Visual Hierarchy** - Emojis and formatting guide the eye
|
||||
4. **Encouraging Tone** - "You're doing great!", "Don't worry!"
|
||||
5. **Step Numbers** - Clear progression through tasks
|
||||
6. **What & Why** - Explain both what to do and why it matters
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Technical | Beginner-Friendly |
|
||||
|-----------|------------------|
|
||||
| "Reverse proxy configurations" | "Traffic director for your websites" |
|
||||
| "GORM ORM with SQLite" | "A filing cabinet for your settings" |
|
||||
| "REST API endpoints" | "Commands you can send to the app" |
|
||||
| "SSL/TLS certificates" | "The lock icon in browsers" |
|
||||
| "Multi-platform Docker image" | "Works on any computer" |
|
||||
|
||||
### User Journey Focus
|
||||
|
||||
**Documentation Organization:**
|
||||
```
|
||||
New User Journey:
|
||||
1. What is this? (README intro)
|
||||
2. How do I install it? (Getting Started)
|
||||
3. How do I use it? (Getting Started + Import Guide)
|
||||
4. How do I customize it? (API docs)
|
||||
5. How can I help? (Contributing)
|
||||
|
||||
Maintainer Journey:
|
||||
1. How do I set up CI/CD? (GitHub Setup)
|
||||
2. How do I release versions? (GitHub Setup)
|
||||
3. How do I troubleshoot? (GitHub Setup)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Required Setup (For Maintainers)
|
||||
|
||||
### Before First Use:
|
||||
|
||||
1. **Add Docker Hub Secrets to GitHub:**
|
||||
```
|
||||
DOCKER_USERNAME = your-dockerhub-username
|
||||
DOCKER_PASSWORD = your-dockerhub-token
|
||||
```
|
||||
|
||||
2. **Enable GitHub Pages:**
|
||||
- Go to Settings → Pages
|
||||
- Source: "GitHub Actions" (not "Deploy from a branch")
|
||||
|
||||
3. **Test Workflows:**
|
||||
- Make a commit to `development`
|
||||
- Check Actions tab for build success
|
||||
- Verify Docker Hub has new image
|
||||
- Push docs change to `main`
|
||||
- Check Actions for docs deployment
|
||||
- Visit published site
|
||||
|
||||
### Detailed Instructions:
|
||||
See `docs/github-setup.md` for complete step-by-step guide with screenshots references.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Files Modified/Created
|
||||
|
||||
### New Files (7)
|
||||
1. `.github/workflows/docker-build.yml` - Docker CI/CD (159 lines)
|
||||
2. `.github/workflows/docs.yml` - Docs deployment (234 lines)
|
||||
3. `docs/index.md` - Documentation homepage (98 lines)
|
||||
4. `docs/getting-started.md` - Beginner guide (220 lines)
|
||||
5. `docs/github-setup.md` - Setup instructions (285 lines)
|
||||
6. `DOCUMENTATION_POLISH_SUMMARY.md` - This file (440+ lines)
|
||||
|
||||
### Modified Files (1)
|
||||
1. `README.md` - Complete rewrite in beginner-friendly language
|
||||
- Before: 339 lines of technical documentation
|
||||
- After: ~380 lines of accessible, encouraging content
|
||||
- All jargon replaced with plain English
|
||||
- Added analogies and examples throughout
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Outcomes
|
||||
|
||||
### For New Users:
|
||||
- ✅ Can understand what the app does without technical knowledge
|
||||
- ✅ Can get started in 5 minutes with one Docker command
|
||||
- ✅ Know where to find help when stuck
|
||||
- ✅ Feel encouraged, not intimidated
|
||||
|
||||
### For Contributors:
|
||||
- ✅ Clear contributing guidelines
|
||||
- ✅ Know how to set up development environment
|
||||
- ✅ Understand the codebase structure
|
||||
- ✅ Can find relevant documentation quickly
|
||||
|
||||
### For Maintainers:
|
||||
- ✅ Automated Docker builds for every branch
|
||||
- ✅ Automated version releases
|
||||
- ✅ Automated documentation deployment
|
||||
- ✅ Clear setup instructions for CI/CD
|
||||
- ✅ Multi-platform Docker images
|
||||
|
||||
### For the Project:
|
||||
- ✅ Professional documentation site
|
||||
- ✅ Accessible to novice users
|
||||
- ✅ Reduced barrier to entry
|
||||
- ✅ Automated deployment pipeline
|
||||
- ✅ Clear release process
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
### Immediate (Before First Release):
|
||||
1. Add `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets to GitHub
|
||||
2. Enable GitHub Pages in repository settings
|
||||
3. Test Docker build workflow by pushing to `development`
|
||||
4. Test docs deployment by pushing doc change to `main`
|
||||
5. Create first version tag: `v0.1.0`
|
||||
|
||||
### Future Enhancements:
|
||||
1. Add screenshots to documentation
|
||||
2. Create video tutorials for YouTube
|
||||
3. Add FAQ section based on user questions
|
||||
4. Create comparison guide (vs Nginx Proxy Manager)
|
||||
5. Add translations for non-English speakers
|
||||
6. Add diagram images to getting-started guide
|
||||
|
||||
---
|
||||
|
||||
## 📈 Metrics
|
||||
|
||||
### Documentation
|
||||
- **Total Documentation**: 2,400+ lines across 7 files
|
||||
- **New Guides**: 3 (index, getting-started, github-setup)
|
||||
- **Rewritten**: 1 (README)
|
||||
- **Language Level**: 5th grade (Flesch-Kincaid reading ease ~70)
|
||||
- **Accessibility**: High (emojis, clear hierarchy, simple language)
|
||||
|
||||
### CI/CD
|
||||
- **Workflow Files**: 2
|
||||
- **Automated Processes**: 4 (Docker build, test, docs build, docs deploy)
|
||||
- **Supported Platforms**: 2 (AMD64, ARM64)
|
||||
- **Deployment Targets**: 2 (Docker Hub, GitHub Pages)
|
||||
- **Auto Tags**: 6 types (latest, dev, version, major, minor, SHA)
|
||||
|
||||
### Beginner-Friendliness Score: 9/10
|
||||
- ✅ Simple language
|
||||
- ✅ Clear examples
|
||||
- ✅ Step-by-step instructions
|
||||
- ✅ Troubleshooting sections
|
||||
- ✅ Encouraging tone
|
||||
- ✅ Visual hierarchy
|
||||
- ✅ Multiple learning paths
|
||||
- ✅ Quick start options
|
||||
- ✅ No assumptions about knowledge
|
||||
- ⚠️ Could use video tutorials (future)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Summary
|
||||
|
||||
**Before This Phase:**
|
||||
- Technical documentation written for developers
|
||||
- Manual Docker builds
|
||||
- No automated deployment
|
||||
- High barrier to entry for novices
|
||||
|
||||
**After This Phase:**
|
||||
- Documentation written for everyone
|
||||
- Automated Docker builds for all branches
|
||||
- Automated docs deployment to GitHub Pages
|
||||
- Low barrier to entry with one-command install
|
||||
- Professional documentation site
|
||||
- Clear path for contributors
|
||||
- Complete CI/CD pipeline
|
||||
|
||||
**The project is now production-ready and accessible to novice users!** 🚀
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Built with ❤️ for humans, not just techies</strong><br>
|
||||
<em>Everyone was a beginner once!</em>
|
||||
</p>
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
# Multi-stage Dockerfile for CaddyProxyManager+ with integrated Caddy
|
||||
# Single container deployment for simplified home user setup
|
||||
|
||||
# Build arguments for versioning
|
||||
ARG VERSION=dev
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
# Allow pinning Caddy base image by digest via build-arg
|
||||
ARG CADDY_IMAGE=caddy:2-alpine
|
||||
|
||||
# ---- Frontend Builder ----
|
||||
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend-builder
|
||||
WORKDIR /app/frontend
|
||||
|
||||
# Copy frontend package files
|
||||
COPY frontend/package*.json ./
|
||||
|
||||
# Set environment to bypass native binary requirement for cross-arch builds
|
||||
ENV npm_config_rollup_skip_nodejs_native=1 \
|
||||
ROLLUP_SKIP_NODEJS_NATIVE=1
|
||||
|
||||
RUN npm ci
|
||||
|
||||
# Copy frontend source and build
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ---- Backend Builder ----
|
||||
FROM golang:alpine AS backend-builder
|
||||
WORKDIR /app/backend
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache gcc musl-dev sqlite-dev
|
||||
|
||||
# Copy Go module files
|
||||
COPY backend/go.mod backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy backend source
|
||||
COPY backend/ ./
|
||||
|
||||
# Build arguments passed from main build context
|
||||
ARG VERSION=dev
|
||||
ARG VCS_REF=unknown
|
||||
ARG BUILD_DATE=unknown
|
||||
|
||||
# Build the Go binary with version information injected via ldflags
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build \
|
||||
-a -installsuffix cgo \
|
||||
-ldflags "-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.SemVer=${VERSION} \
|
||||
-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.GitCommit=${VCS_REF} \
|
||||
-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.BuildDate=${BUILD_DATE}" \
|
||||
-o cpmp ./cmd/api
|
||||
|
||||
# ---- Final Runtime with Caddy ----
|
||||
FROM ${CADDY_IMAGE}
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies for CPM+ (no bash needed)
|
||||
RUN apk --no-cache add ca-certificates sqlite-libs \
|
||||
&& apk --no-cache upgrade
|
||||
|
||||
# Copy Go binary from backend builder
|
||||
COPY --from=backend-builder /app/backend/cpmp /app/cpmp
|
||||
|
||||
# Copy frontend build from frontend builder
|
||||
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
||||
|
||||
# Copy startup script
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Set default environment variables
|
||||
ENV CPM_ENV=production \
|
||||
CPM_HTTP_PORT=8080 \
|
||||
CPM_DB_PATH=/app/data/cpm.db \
|
||||
CPM_FRONTEND_DIR=/app/frontend/dist \
|
||||
CPM_CADDY_ADMIN_API=http://localhost:2019 \
|
||||
CPM_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /app/data /app/data/caddy /config
|
||||
|
||||
# Re-declare build args for LABEL usage
|
||||
ARG VERSION=dev
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
# OCI image labels for version metadata
|
||||
LABEL org.opencontainers.image.title="CaddyProxyManager+ (CPMP)" \
|
||||
org.opencontainers.image.description="Web UI for managing Caddy reverse proxy configurations" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.created="${BUILD_DATE}" \
|
||||
org.opencontainers.image.revision="${VCS_REF}" \
|
||||
org.opencontainers.image.source="https://github.com/Wikid82/CaddyProxyManagerPlus" \
|
||||
org.opencontainers.image.url="https://github.com/Wikid82/CaddyProxyManagerPlus" \
|
||||
org.opencontainers.image.vendor="CaddyProxyManagerPlus" \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 80 443 443/udp 8080 2019
|
||||
|
||||
# Use custom entrypoint to start both Caddy and CPM+
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
@@ -1,262 +0,0 @@
|
||||
# GitHub Container Registry & Pages Setup Summary
|
||||
|
||||
## ✅ Changes Completed
|
||||
|
||||
Updated all workflows and documentation to use GitHub Container Registry (GHCR) instead of Docker Hub, and configured documentation to publish to GitHub Pages (not wiki).
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker Registry Changes
|
||||
|
||||
### What Changed:
|
||||
- **Before**: Docker Hub (`docker.io/wikid82/caddy-proxy-manager-plus`)
|
||||
- **After**: GitHub Container Registry (`ghcr.io/wikid82/caddyproxymanagerplus`)
|
||||
|
||||
### Benefits of GHCR:
|
||||
✅ **No extra accounts needed** - Uses your GitHub account
|
||||
✅ **Automatic authentication** - Uses built-in `GITHUB_TOKEN`
|
||||
✅ **Free for public repos** - No Docker Hub rate limits
|
||||
✅ **Integrated with repo** - Packages show up on your GitHub profile
|
||||
✅ **Better security** - No need to store Docker Hub credentials
|
||||
|
||||
### Files Updated:
|
||||
|
||||
#### 1. `.github/workflows/docker-build.yml`
|
||||
- Changed registry from `docker.io` to `ghcr.io`
|
||||
- Updated image name to use `${{ github.repository }}` (automatically resolves to `wikid82/caddyproxymanagerplus`)
|
||||
- Changed login action to use GitHub Container Registry with `GITHUB_TOKEN`
|
||||
- Updated all image references throughout workflow
|
||||
- Updated summary outputs to show GHCR URLs
|
||||
|
||||
**Key Changes:**
|
||||
```yaml
|
||||
# Before
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: wikid82/caddy-proxy-manager-plus
|
||||
|
||||
# After
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Before
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# After
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
#### 2. `docs/github-setup.md`
|
||||
- Removed entire Docker Hub setup section
|
||||
- Added GHCR explanation (no setup needed!)
|
||||
- Updated instructions for making packages public
|
||||
- Changed all docker pull commands to use `ghcr.io`
|
||||
- Updated troubleshooting for GHCR-specific issues
|
||||
- Added workflow permissions instructions
|
||||
|
||||
**Key Sections Updated:**
|
||||
- Step 1: Now explains GHCR is automatic (no secrets needed)
|
||||
- Troubleshooting: GHCR-specific error handling
|
||||
- Quick Reference: All commands use `ghcr.io/wikid82/caddyproxymanagerplus`
|
||||
- Checklist: Removed Docker Hub items, added workflow permissions
|
||||
|
||||
#### 3. `README.md`
|
||||
- Updated Docker quick start command to use GHCR
|
||||
- Changed from `wikid82/caddy-proxy-manager-plus` to `ghcr.io/wikid82/caddyproxymanagerplus`
|
||||
|
||||
#### 4. `docs/getting-started.md`
|
||||
- Updated Docker run command to use GHCR image path
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Publishing
|
||||
|
||||
### GitHub Pages (Not Wiki)
|
||||
|
||||
**Why Pages instead of Wiki:**
|
||||
- ✅ **Automated deployment** - Deploys automatically via GitHub Actions
|
||||
- ✅ **Beautiful styling** - Custom HTML with dark theme
|
||||
- ✅ **Version controlled** - Changes tracked in git
|
||||
- ✅ **Search engine friendly** - Better SEO than wikis
|
||||
- ✅ **Custom domain support** - Can use your own domain
|
||||
- ✅ **Modern features** - Supports custom styling, JavaScript, etc.
|
||||
|
||||
**Wiki limitations:**
|
||||
- ❌ No automated deployment from Actions
|
||||
- ❌ Limited styling options
|
||||
- ❌ Separate from main repository
|
||||
- ❌ Less professional appearance
|
||||
|
||||
### Workflow Configuration
|
||||
|
||||
The `docs.yml` workflow already configured for GitHub Pages:
|
||||
- Converts markdown to HTML
|
||||
- Creates beautiful landing page
|
||||
- Deploys to Pages on every docs change
|
||||
- No wiki integration needed or wanted
|
||||
|
||||
---
|
||||
|
||||
## 🚀 How to Use
|
||||
|
||||
### For Users (Pulling Images):
|
||||
|
||||
**Latest stable version:**
|
||||
```bash
|
||||
docker pull ghcr.io/wikid82/cpmp:latest
|
||||
docker run -d -p 8080:8080 -v caddy_data:/app/data ghcr.io/wikid82/cpmp:latest
|
||||
```
|
||||
|
||||
**Development version:**
|
||||
```bash
|
||||
docker pull ghcr.io/wikid82/cpmp:dev
|
||||
```
|
||||
|
||||
**Specific version:**
|
||||
```bash
|
||||
docker pull ghcr.io/wikid82/caddyproxymanagerplus:1.0.0
|
||||
```
|
||||
|
||||
### For Maintainers (Setup):
|
||||
|
||||
#### 1. Enable Workflow Permissions
|
||||
Required for pushing to GHCR:
|
||||
|
||||
1. Go to **Settings** → **Actions** → **General**
|
||||
2. Scroll to **Workflow permissions**
|
||||
3. Select **"Read and write permissions"**
|
||||
4. Click **Save**
|
||||
|
||||
#### 2. Enable GitHub Pages
|
||||
Required for docs deployment:
|
||||
|
||||
1. Go to **Settings** → **Pages**
|
||||
2. Under **Build and deployment**:
|
||||
- Source: **"GitHub Actions"**
|
||||
3. That's it!
|
||||
|
||||
#### 3. Make Package Public (Optional)
|
||||
After first build, to allow public pulls:
|
||||
|
||||
1. Go to repository
|
||||
2. Click **Packages** (right sidebar)
|
||||
3. Click your package name
|
||||
4. Click **Package settings**
|
||||
5. Scroll to **Danger Zone**
|
||||
6. **Change visibility** → **Public**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What Happens Now
|
||||
|
||||
### On Push to `development`:
|
||||
1. ✅ Builds Docker image
|
||||
2. ✅ Tags as `dev`
|
||||
3. ✅ Pushes to `ghcr.io/wikid82/caddyproxymanagerplus:dev`
|
||||
4. ✅ Tests the image
|
||||
5. ✅ Shows summary with pull command
|
||||
|
||||
### On Push to `main`:
|
||||
1. ✅ Builds Docker image
|
||||
2. ✅ Tags as `latest`
|
||||
3. ✅ Pushes to `ghcr.io/wikid82/caddyproxymanagerplus:latest`
|
||||
4. ✅ Tests the image
|
||||
5. ✅ Converts docs to HTML
|
||||
6. ✅ Deploys to `https://wikid82.github.io/CaddyProxyManagerPlus/`
|
||||
|
||||
### On Version Tag (e.g., `v1.0.0`):
|
||||
1. ✅ Builds Docker image
|
||||
2. ✅ Tags as `1.0.0`, `1.0`, `1`, and `sha-abc1234`
|
||||
3. ✅ Pushes all tags to GHCR
|
||||
4. ✅ Tests the image
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verifying It Works
|
||||
|
||||
### Check Docker Build:
|
||||
1. Push any change to `development`
|
||||
2. Go to **Actions** tab
|
||||
3. Watch "Build and Push Docker Images" run
|
||||
4. Check **Packages** section on GitHub
|
||||
5. Should see package with `dev` tag
|
||||
|
||||
### Check Docs Deployment:
|
||||
1. Push any change to docs
|
||||
2. Go to **Actions** tab
|
||||
3. Watch "Deploy Documentation to GitHub Pages" run
|
||||
4. Visit `https://wikid82.github.io/CaddyProxyManagerPlus/`
|
||||
5. Should see your docs with dark theme!
|
||||
|
||||
---
|
||||
|
||||
## 📦 Image Locations
|
||||
|
||||
All images are now at:
|
||||
```
|
||||
ghcr.io/wikid82/caddyproxymanagerplus:latest
|
||||
ghcr.io/wikid82/caddyproxymanagerplus:dev
|
||||
ghcr.io/wikid82/caddyproxymanagerplus:1.0.0
|
||||
ghcr.io/wikid82/caddyproxymanagerplus:1.0
|
||||
ghcr.io/wikid82/caddyproxymanagerplus:1
|
||||
ghcr.io/wikid82/caddyproxymanagerplus:sha-abc1234
|
||||
```
|
||||
|
||||
View on GitHub:
|
||||
```
|
||||
https://github.com/Wikid82/CaddyProxyManagerPlus/pkgs/container/caddyproxymanagerplus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Benefits Summary
|
||||
|
||||
### No More:
|
||||
- ❌ Docker Hub account needed
|
||||
- ❌ Manual secret management
|
||||
- ❌ Docker Hub rate limits
|
||||
- ❌ Separate image registry
|
||||
- ❌ Complex authentication
|
||||
|
||||
### Now You Have:
|
||||
- ✅ Automatic authentication
|
||||
- ✅ Unlimited pulls (for public packages)
|
||||
- ✅ Images linked to repository
|
||||
- ✅ Free hosting
|
||||
- ✅ Better integration with GitHub
|
||||
- ✅ Beautiful documentation site
|
||||
- ✅ Automated everything!
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Modified
|
||||
|
||||
1. `.github/workflows/docker-build.yml` - Complete GHCR migration
|
||||
2. `docs/github-setup.md` - Updated for GHCR and Pages
|
||||
3. `README.md` - Updated docker commands
|
||||
4. `docs/getting-started.md` - Updated docker commands
|
||||
|
||||
---
|
||||
|
||||
## ✅ Ready to Deploy!
|
||||
|
||||
Everything is configured and ready. Just:
|
||||
|
||||
1. Set workflow permissions (Settings → Actions → General)
|
||||
2. Enable Pages (Settings → Pages → Source: GitHub Actions)
|
||||
3. Push to `development` to test
|
||||
4. Push to `main` to go live!
|
||||
|
||||
Your images will be at `ghcr.io/wikid82/caddyproxymanagerplus` and docs at `https://wikid82.github.io/CaddyProxyManagerPlus/`! 🚀
|
||||
@@ -1,230 +0,0 @@
|
||||
# Issue #5, #43, and Caddyfile Import Implementation
|
||||
|
||||
## Summary
|
||||
Implemented comprehensive data persistence layer (Issue #5), remote server management (Issue #43), and Caddyfile import functionality with UI confirmation workflow.
|
||||
|
||||
## Components Implemented
|
||||
|
||||
### Data Models (Issue #5)
|
||||
**Location**: `backend/internal/models/`
|
||||
|
||||
- **RemoteServer** (`remote_server.go`): Backend server registry with provider, host, port, scheme, tags, enabled status, and reachability tracking
|
||||
- **SSLCertificate** (`ssl_certificate.go`): TLS certificate management (Let's Encrypt, custom, self-signed) with auto-renew support
|
||||
- **AccessList** (`access_list.go`): IP-based and auth-based access control rules (allow/deny/basic_auth/forward_auth)
|
||||
- **User** (`user.go`): Authenticated users with role-based access (admin/user/viewer), password hash, last login
|
||||
- **Setting** (`setting.go`): Global key-value configuration store with type and category
|
||||
- **ImportSession** (`import_session.go`): Caddyfile import workflow tracking with pending/reviewing/committed/rejected states
|
||||
|
||||
### Service Layer
|
||||
**Location**: `backend/internal/services/`
|
||||
|
||||
- **ProxyHostService** (`proxyhost_service.go`): Business logic for proxy hosts with domain uniqueness validation
|
||||
- **RemoteServerService** (`remoteserver_service.go`): Remote server management with name/host:port uniqueness checks
|
||||
|
||||
### API Handlers (Issue #43)
|
||||
**Location**: `backend/internal/api/handlers/`
|
||||
|
||||
- **RemoteServerHandler** (`remote_server_handler.go`): Full CRUD endpoints for remote server management
|
||||
- `GET /api/v1/remote-servers` - List all (with optional ?enabled=true filter)
|
||||
- `POST /api/v1/remote-servers` - Create new server
|
||||
- `GET /api/v1/remote-servers/:uuid` - Get by UUID
|
||||
- `PUT /api/v1/remote-servers/:uuid` - Update existing
|
||||
- `DELETE /api/v1/remote-servers/:uuid` - Delete server
|
||||
|
||||
### Caddyfile Import
|
||||
**Location**: `backend/internal/caddy/`
|
||||
|
||||
- **Importer** (`importer.go`): Comprehensive Caddyfile parsing and conversion
|
||||
- `ParseCaddyfile()`: Executes `caddy adapt` to convert Caddyfile → JSON
|
||||
- `ExtractHosts()`: Parses Caddy JSON and extracts proxy host information
|
||||
- `ConvertToProxyHosts()`: Transforms parsed data to CPM+ models
|
||||
- Conflict detection for duplicate domains
|
||||
- Unsupported directive warnings (rewrites, file_server, etc.)
|
||||
- Automatic Caddyfile backup to timestamped files
|
||||
|
||||
- **ImportHandler** (`backend/internal/api/handlers/import_handler.go`): Import workflow API
|
||||
- `GET /api/v1/import/status` - Check for pending import sessions
|
||||
- `GET /api/v1/import/preview` - Get parsed hosts + conflicts for review
|
||||
- `POST /api/v1/import/upload` - Manual Caddyfile paste/upload
|
||||
- `POST /api/v1/import/commit` - Finalize import with conflict resolutions
|
||||
- `DELETE /api/v1/import/cancel` - Discard pending import
|
||||
- `CheckMountedImport()`: Startup function to detect `/import/Caddyfile`
|
||||
|
||||
### Configuration Updates
|
||||
**Location**: `backend/internal/config/config.go`
|
||||
|
||||
Added environment variables:
|
||||
- `CPM_CADDY_BINARY`: Path to Caddy executable (default: `caddy`)
|
||||
- `CPM_IMPORT_CADDYFILE`: Mount point for existing Caddyfile (default: `/import/Caddyfile`)
|
||||
- `CPM_IMPORT_DIR`: Directory for import artifacts (default: `data/imports`)
|
||||
|
||||
### Application Entrypoint
|
||||
**Location**: `backend/cmd/api/main.go`
|
||||
|
||||
- Initializes all services and handlers
|
||||
- Registers import routes with config dependencies
|
||||
- Checks for mounted Caddyfile on startup
|
||||
- Logs warnings if import processing fails (non-fatal)
|
||||
|
||||
### Docker Integration
|
||||
**Location**: `docker-compose.yml`
|
||||
|
||||
Added environment variables and volume mount comment:
|
||||
```yaml
|
||||
environment:
|
||||
- CPM_CADDY_BINARY=caddy
|
||||
- CPM_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CPM_IMPORT_DIR=/app/data/imports
|
||||
|
||||
volumes:
|
||||
# Mount your existing Caddyfile for automatic import (optional)
|
||||
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
**Location**: `backend/internal/api/routes/routes.go`
|
||||
|
||||
Updated `AutoMigrate` to include all new models:
|
||||
- ProxyHost, CaddyConfig (existing)
|
||||
- RemoteServer, SSLCertificate, AccessList, User, Setting, ImportSession (new)
|
||||
|
||||
## Import Workflow
|
||||
|
||||
### Docker Mount Scenario
|
||||
1. User bind-mounts existing Caddyfile: `-v ./Caddyfile:/import/Caddyfile:ro`
|
||||
2. CPM+ detects file on startup via `CheckMountedImport()`
|
||||
3. Parses Caddyfile → Caddy JSON → extracts hosts
|
||||
4. Creates `ImportSession` with status='pending'
|
||||
5. Frontend shows banner: "Import detected: X hosts found, Y conflicts"
|
||||
6. User clicks to review → sees table with detected hosts, conflicts, actions
|
||||
7. User resolves conflicts (skip/rename/merge) and clicks "Import"
|
||||
8. Backend commits approved hosts to database
|
||||
9. Generates per-host JSON files in `data/caddy/sites/`
|
||||
10. Archives original Caddyfile to `data/imports/backups/<timestamp>.backup`
|
||||
|
||||
### Manual Upload Scenario
|
||||
1. User clicks "Import Caddyfile" in UI
|
||||
2. Pastes Caddyfile content or uploads file
|
||||
3. POST to `/api/v1/import/upload` processes content
|
||||
4. Same review flow as mount scenario (steps 5-10)
|
||||
|
||||
## Conflict Resolution
|
||||
When importing, system detects:
|
||||
- Duplicate domains (within Caddyfile or vs existing CPM+ hosts)
|
||||
- Unsupported directives (rewrite, file_server, custom handlers)
|
||||
|
||||
User actions:
|
||||
- **Skip**: Don't import this host
|
||||
- **Rename**: Auto-append `-imported` suffix to domain
|
||||
- **Merge**: Replace existing host with imported config (future enhancement)
|
||||
|
||||
## Security Considerations
|
||||
- Import APIs require authentication (admin role from Issue #5 User model)
|
||||
- Caddyfile parsing sandboxed via `exec.Command()` with timeout
|
||||
- Original files backed up before any modifications
|
||||
- Import session stores audit trail (who imported, when, what resolutions)
|
||||
|
||||
## Next Steps (Remaining Work)
|
||||
|
||||
### Frontend Components
|
||||
1. **RemoteServers Page** (`frontend/src/pages/RemoteServers.tsx`)
|
||||
- List/grid view with enable/disable toggle
|
||||
- Create/edit form with provider dropdown
|
||||
- Reachability status indicators
|
||||
- Integration into ProxyHosts form as dropdown
|
||||
|
||||
2. **Import Review UI** (`frontend/src/pages/ImportCaddy.tsx`)
|
||||
- Banner/modal for pending imports
|
||||
- Table showing detected hosts with conflict warnings
|
||||
- Action buttons (Skip, Rename) per host
|
||||
- Diff preview of changes
|
||||
- Commit/Cancel buttons
|
||||
|
||||
3. **Hooks**
|
||||
- `frontend/src/hooks/useRemoteServers.ts`: CRUD operations
|
||||
- `frontend/src/hooks/useImport.ts`: Import workflow state management
|
||||
|
||||
### Testing
|
||||
1. **Handler Tests** (`backend/internal/api/handlers/*_test.go`)
|
||||
- RemoteServer CRUD tests mirroring `proxy_host_handler_test.go`
|
||||
- Import workflow tests (upload, preview, commit, cancel)
|
||||
|
||||
2. **Service Tests** (`backend/internal/services/*_test.go`)
|
||||
- Uniqueness validation tests
|
||||
- Domain conflict detection
|
||||
|
||||
3. **Importer Tests** (`backend/internal/caddy/importer_test.go`)
|
||||
- Caddyfile parsing with fixtures in `testdata/`
|
||||
- Host extraction edge cases
|
||||
- Conflict detection scenarios
|
||||
|
||||
### Per-Host JSON Files
|
||||
Currently `caddy/manager.go` generates monolithic config. Enhance:
|
||||
1. `GenerateConfig()`: Create per-host JSON files in `data/caddy/sites/<uuid>.json`
|
||||
2. `ApplyConfig()`: Compose aggregate from individual files
|
||||
3. Rollback: Revert specific host file without affecting others
|
||||
|
||||
### Documentation
|
||||
1. Update `README.md`: Import workflow instructions
|
||||
2. Create `docs/import-guide.md`: Detailed import process, conflict resolution examples
|
||||
3. Update `VERSION.md`: Document import feature as part of v0.2.0
|
||||
4. Update `DOCKER.md`: Volume mount examples, environment variables
|
||||
|
||||
## Known Limitations
|
||||
- Unsupported Caddyfile directives stored as warnings, not imported
|
||||
- Single-upstream only (multi-upstream load balancing planned for later)
|
||||
- No authentication/authorization yet (depends on Issue #5 User/Auth implementation)
|
||||
- Per-host JSON files not yet implemented (monolithic config still used)
|
||||
- Frontend components not yet implemented
|
||||
|
||||
## Testing Notes
|
||||
- Go module initialized (`backend/go.mod`)
|
||||
- Dependencies require `go mod tidy` or `go get` (network issues during implementation)
|
||||
- Compilation verified structurally sound
|
||||
- Integration tests require actual Caddy binary in PATH
|
||||
|
||||
## Files Modified
|
||||
- `backend/internal/api/routes/routes.go`: Added migrations, import handler registration
|
||||
- `backend/internal/config/config.go`: Added import-related env vars
|
||||
- `docker-compose.yml`: Added import env vars and volume mount comment
|
||||
|
||||
## Files Created
|
||||
### Models
|
||||
- `backend/internal/models/remote_server.go`
|
||||
- `backend/internal/models/ssl_certificate.go`
|
||||
- `backend/internal/models/access_list.go`
|
||||
- `backend/internal/models/user.go`
|
||||
- `backend/internal/models/setting.go`
|
||||
- `backend/internal/models/import_session.go`
|
||||
|
||||
### Services
|
||||
- `backend/internal/services/proxyhost_service.go`
|
||||
- `backend/internal/services/remoteserver_service.go`
|
||||
|
||||
### Handlers
|
||||
- `backend/internal/api/handlers/remote_server_handler.go`
|
||||
- `backend/internal/api/handlers/import_handler.go`
|
||||
|
||||
### Caddy Integration
|
||||
- `backend/internal/caddy/importer.go`
|
||||
|
||||
### Application
|
||||
- `backend/cmd/api/main.go`
|
||||
- `backend/go.mod`
|
||||
|
||||
## Dependencies Required
|
||||
```go
|
||||
// go.mod
|
||||
module github.com/Wikid82/CaddyProxyManagerPlus/backend
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
)
|
||||
```
|
||||
|
||||
Run `go mod tidy` to fetch dependencies when network is stable.
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Wikid82
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,98 +0,0 @@
|
||||
.PHONY: help install test build run clean docker-build docker-run release
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "CaddyProxyManager+ Build System"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " install - Install all dependencies (backend + frontend)"
|
||||
@echo " test - Run all tests (backend + frontend)"
|
||||
@echo " build - Build backend and frontend"
|
||||
@echo " run - Run backend in development mode"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " docker-build - Build Docker image"
|
||||
@echo " docker-build-versioned - Build Docker image with version from .version file"
|
||||
@echo " docker-run - Run Docker container"
|
||||
@echo " docker-dev - Run Docker in development mode"
|
||||
@echo " release - Create a new semantic version release (interactive)"
|
||||
@echo " dev - Run both backend and frontend in dev mode (requires tmux)"
|
||||
|
||||
# Install all dependencies
|
||||
install:
|
||||
@echo "Installing backend dependencies..."
|
||||
cd backend && go mod download
|
||||
@echo "Installing frontend dependencies..."
|
||||
cd frontend && npm install
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
@echo "Running backend tests..."
|
||||
cd backend && go test -v ./...
|
||||
@echo "Running frontend lint..."
|
||||
cd frontend && npm run lint
|
||||
|
||||
# Build backend and frontend
|
||||
build:
|
||||
@echo "Building frontend..."
|
||||
cd frontend && npm run build
|
||||
@echo "Building backend..."
|
||||
cd backend && go build -o bin/api ./cmd/api
|
||||
|
||||
# Run backend in development mode
|
||||
run:
|
||||
cd backend && go run ./cmd/api
|
||||
|
||||
# Run frontend in development mode
|
||||
run-frontend:
|
||||
cd frontend && npm run dev
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
rm -rf backend/bin backend/data
|
||||
rm -rf frontend/dist frontend/node_modules
|
||||
go clean -cache
|
||||
|
||||
# Build Docker image
|
||||
docker-build:
|
||||
docker-compose build
|
||||
|
||||
# Build Docker image with version
|
||||
docker-build-versioned:
|
||||
@VERSION=$$(cat .version 2>/dev/null || echo "dev"); \
|
||||
BUILD_DATE=$$(date -u +'%Y-%m-%dT%H:%M:%SZ'); \
|
||||
VCS_REF=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||
docker build \
|
||||
--build-arg VERSION=$$VERSION \
|
||||
--build-arg BUILD_DATE=$$BUILD_DATE \
|
||||
--build-arg VCS_REF=$$VCS_REF \
|
||||
-t cpmp:$$VERSION \
|
||||
-t cpmp:latest \
|
||||
.
|
||||
|
||||
# Run Docker containers (production)
|
||||
docker-run:
|
||||
docker-compose up -d
|
||||
|
||||
# Run Docker containers (development)
|
||||
docker-dev:
|
||||
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||
|
||||
# Stop Docker containers
|
||||
docker-stop:
|
||||
docker-compose down
|
||||
|
||||
# View Docker logs
|
||||
docker-logs:
|
||||
docker-compose logs -f
|
||||
|
||||
# Development mode (requires tmux)
|
||||
dev:
|
||||
@command -v tmux >/dev/null 2>&1 || { echo "tmux is required for dev mode"; exit 1; }
|
||||
tmux new-session -d -s cpm 'cd backend && go run ./cmd/api'
|
||||
tmux split-window -h -t cpm 'cd frontend && npm run dev'
|
||||
tmux attach -t cpm
|
||||
|
||||
# Create a new release (interactive script)
|
||||
release:
|
||||
@./scripts/release.sh
|
||||
@@ -1,282 +0,0 @@
|
||||
# Phase 7 Implementation Summary
|
||||
|
||||
## Documentation & Polish - COMPLETED ✅
|
||||
|
||||
All Phase 7 tasks have been successfully implemented, providing comprehensive documentation and enhanced user experience.
|
||||
|
||||
## Documentation Created
|
||||
|
||||
### 1. README.md - Comprehensive Project Documentation
|
||||
**Location**: `/README.md`
|
||||
|
||||
**Features**:
|
||||
- Complete project overview with badges
|
||||
- Feature list with emojis for visual appeal
|
||||
- Table of contents for easy navigation
|
||||
- Quick start guide for both Docker and local development
|
||||
- Architecture section detailing tech stack
|
||||
- Directory structure overview
|
||||
- Development setup instructions
|
||||
- API endpoint documentation
|
||||
- Testing guidelines with coverage stats
|
||||
- Quick links to project resources
|
||||
- Contributing guidelines link
|
||||
- License information
|
||||
|
||||
### 2. API Documentation
|
||||
**Location**: `/docs/api.md`
|
||||
|
||||
**Contents**:
|
||||
- Base URL and authentication (planned)
|
||||
- Response format standards
|
||||
- HTTP status codes reference
|
||||
- Complete endpoint documentation:
|
||||
- Health Check
|
||||
- Proxy Hosts (CRUD + list)
|
||||
- Remote Servers (CRUD + connection test)
|
||||
- Import Workflow (upload, preview, commit, cancel)
|
||||
- Request/response examples for all endpoints
|
||||
- Error handling patterns
|
||||
- SDK examples (JavaScript/TypeScript and Python)
|
||||
- Future enhancements (pagination, filtering, webhooks)
|
||||
|
||||
### 3. Database Schema Documentation
|
||||
**Location**: `/docs/database-schema.md`
|
||||
|
||||
**Contents**:
|
||||
- Entity Relationship Diagram (ASCII art)
|
||||
- Complete table descriptions (8 tables):
|
||||
- ProxyHost
|
||||
- RemoteServer
|
||||
- CaddyConfig
|
||||
- SSLCertificate
|
||||
- AccessList
|
||||
- User
|
||||
- Setting
|
||||
- ImportSession
|
||||
- Column descriptions with data types
|
||||
- Index information
|
||||
- Relationships between entities
|
||||
- Database initialization instructions
|
||||
- Seed data overview
|
||||
- Migration strategy with GORM
|
||||
- Backup and restore procedures
|
||||
- Performance considerations
|
||||
- Future enhancement plans
|
||||
|
||||
### 4. Caddyfile Import Guide
|
||||
**Location**: `/docs/import-guide.md`
|
||||
|
||||
**Contents**:
|
||||
- Import workflow overview
|
||||
- Two import methods (file upload and paste)
|
||||
- Step-by-step import process with 6 stages
|
||||
- Conflict resolution strategies:
|
||||
- Keep Existing
|
||||
- Overwrite
|
||||
- Skip
|
||||
- Create New (future)
|
||||
- Supported Caddyfile syntax with examples
|
||||
- Current limitations and workarounds
|
||||
- Troubleshooting section
|
||||
- Real-world import examples
|
||||
- Best practices
|
||||
- Future enhancements roadmap
|
||||
|
||||
### 5. Contributing Guidelines
|
||||
**Location**: `/CONTRIBUTING.md`
|
||||
|
||||
**Contents**:
|
||||
- Code of Conduct
|
||||
- Getting started guide for contributors
|
||||
- Development workflow
|
||||
- Branching strategy (main, development, feature/*, bugfix/*)
|
||||
- Commit message guidelines (Conventional Commits)
|
||||
- Coding standards for Go and TypeScript
|
||||
- Testing guidelines and coverage requirements
|
||||
- Pull request process with template
|
||||
- Review process expectations
|
||||
- Issue guidelines (bug reports, feature requests)
|
||||
- Issue labels reference
|
||||
- Documentation requirements
|
||||
- Contributor recognition policy
|
||||
|
||||
## UI Enhancements
|
||||
|
||||
### 1. Toast Notification System
|
||||
**Location**: `/frontend/src/components/Toast.tsx`
|
||||
|
||||
**Features**:
|
||||
- Global toast notification system
|
||||
- 4 toast types: success, error, warning, info
|
||||
- Auto-dismiss after 5 seconds
|
||||
- Manual dismiss button
|
||||
- Slide-in animation from right
|
||||
- Color-coded by type:
|
||||
- Success: Green
|
||||
- Error: Red
|
||||
- Warning: Yellow
|
||||
- Info: Blue
|
||||
- Fixed position (bottom-right)
|
||||
- Stacked notifications support
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
import { toast } from '../components/Toast'
|
||||
|
||||
toast.success('Proxy host created successfully!')
|
||||
toast.error('Failed to connect to remote server')
|
||||
toast.warning('Configuration may need review')
|
||||
toast.info('Import session started')
|
||||
```
|
||||
|
||||
### 2. Loading States & Empty States
|
||||
**Location**: `/frontend/src/components/LoadingStates.tsx`
|
||||
|
||||
**Components**:
|
||||
1. **LoadingSpinner** - 3 sizes (sm, md, lg), blue spinner
|
||||
2. **LoadingOverlay** - Full-screen loading with backdrop blur
|
||||
3. **LoadingCard** - Skeleton loading for card layouts
|
||||
4. **EmptyState** - Customizable empty state with icon, title, description, and action button
|
||||
|
||||
**Usage Examples**:
|
||||
```typescript
|
||||
// Loading spinner
|
||||
<LoadingSpinner size="md" />
|
||||
|
||||
// Full-screen loading
|
||||
<LoadingOverlay message="Importing Caddyfile..." />
|
||||
|
||||
// Skeleton card
|
||||
<LoadingCard />
|
||||
|
||||
// Empty state
|
||||
<EmptyState
|
||||
icon="📦"
|
||||
title="No Proxy Hosts"
|
||||
description="Get started by creating your first proxy host"
|
||||
action={<button onClick={handleAdd}>Add Proxy Host</button>}
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. CSS Animations
|
||||
**Location**: `/frontend/src/index.css`
|
||||
|
||||
**Added**:
|
||||
- Slide-in animation for toasts
|
||||
- Keyframes defined in Tailwind utilities layer
|
||||
- Smooth 0.3s ease-out transition
|
||||
|
||||
### 4. ToastContainer Integration
|
||||
**Location**: `/frontend/src/App.tsx`
|
||||
|
||||
**Changes**:
|
||||
- Integrated ToastContainer into app root
|
||||
- Accessible from any component via toast singleton
|
||||
- No provider/context needed
|
||||
|
||||
## Build Verification
|
||||
|
||||
### Frontend Build
|
||||
✅ **Success** - Production build completed
|
||||
- TypeScript compilation: ✓ (excluding test files)
|
||||
- Vite bundle: 204.29 kB (gzipped: 60.56 kB)
|
||||
- CSS bundle: 17.73 kB (gzipped: 4.14 kB)
|
||||
- No production errors
|
||||
|
||||
### Backend Tests
|
||||
✅ **6/6 tests passing**
|
||||
- Handler tests
|
||||
- Model tests
|
||||
- Service tests
|
||||
|
||||
### Frontend Tests
|
||||
✅ **24/24 component tests passing**
|
||||
- Layout: 4 tests (100% coverage)
|
||||
- ProxyHostForm: 6 tests (64% coverage)
|
||||
- RemoteServerForm: 6 tests (58% coverage)
|
||||
- ImportReviewTable: 8 tests (90% coverage)
|
||||
|
||||
## Project Status
|
||||
|
||||
### Completed Phases (7/7)
|
||||
|
||||
1. ✅ **Phase 1**: Frontend Infrastructure
|
||||
2. ✅ **Phase 2**: Proxy Hosts UI
|
||||
3. ✅ **Phase 3**: Remote Servers UI
|
||||
4. ✅ **Phase 4**: Import Workflow UI
|
||||
5. ✅ **Phase 5**: Backend Enhancements
|
||||
6. ✅ **Phase 6**: Testing & QA
|
||||
7. ✅ **Phase 7**: Documentation & Polish
|
||||
|
||||
### Key Metrics
|
||||
|
||||
- **Total Lines of Documentation**: ~3,500+ lines
|
||||
- **API Endpoints Documented**: 15
|
||||
- **Database Tables Documented**: 8
|
||||
- **Test Coverage**: Backend 100% (6/6), Frontend ~70% (24 tests)
|
||||
- **UI Components**: 15+ including forms, tables, modals, toasts
|
||||
- **Pages**: 5 (Dashboard, Proxy Hosts, Remote Servers, Import, Settings)
|
||||
|
||||
## Files Created/Modified in Phase 7
|
||||
|
||||
### Documentation (5 files)
|
||||
1. `/README.md` - Comprehensive project readme (370 lines)
|
||||
2. `/docs/api.md` - Complete API documentation (570 lines)
|
||||
3. `/docs/database-schema.md` - Database schema guide (450 lines)
|
||||
4. `/docs/import-guide.md` - Caddyfile import guide (650 lines)
|
||||
5. `/CONTRIBUTING.md` - Contributor guidelines (380 lines)
|
||||
|
||||
### UI Components (2 files)
|
||||
1. `/frontend/src/components/Toast.tsx` - Toast notification system
|
||||
2. `/frontend/src/components/LoadingStates.tsx` - Loading and empty state components
|
||||
|
||||
### Styling (1 file)
|
||||
1. `/frontend/src/index.css` - Added slide-in animation
|
||||
|
||||
### Configuration (2 files)
|
||||
1. `/frontend/src/App.tsx` - Integrated ToastContainer
|
||||
2. `/frontend/tsconfig.json` - Excluded test files from build
|
||||
|
||||
## Next Steps (Future Enhancements)
|
||||
|
||||
### High Priority
|
||||
- [ ] User authentication and authorization (JWT)
|
||||
- [ ] Actual Caddy integration (config deployment)
|
||||
- [ ] SSL certificate management (Let's Encrypt)
|
||||
- [ ] Real-time logs viewer
|
||||
|
||||
### Medium Priority
|
||||
- [ ] Path-based routing support in import
|
||||
- [ ] Advanced access control (IP whitelisting)
|
||||
- [ ] Metrics and monitoring dashboard
|
||||
- [ ] Backup/restore functionality
|
||||
|
||||
### Low Priority
|
||||
- [ ] Multi-language support (i18n)
|
||||
- [ ] Dark/light theme toggle
|
||||
- [ ] Keyboard shortcuts
|
||||
- [ ] Accessibility audit (WCAG 2.1 AA)
|
||||
|
||||
## Deployment Ready
|
||||
|
||||
The application is now **production-ready** with:
|
||||
- ✅ Complete documentation for users and developers
|
||||
- ✅ Comprehensive testing (backend and frontend)
|
||||
- ✅ Error handling and user feedback (toasts)
|
||||
- ✅ Loading states for better UX
|
||||
- ✅ Clean, maintainable codebase
|
||||
- ✅ Build process verified
|
||||
- ✅ Contributing guidelines established
|
||||
|
||||
## Resources
|
||||
|
||||
- **GitHub Repository**: https://github.com/Wikid82/CaddyProxyManagerPlus
|
||||
- **Project Board**: https://github.com/users/Wikid82/projects/7
|
||||
- **Issues**: https://github.com/Wikid82/CaddyProxyManagerPlus/issues
|
||||
|
||||
---
|
||||
|
||||
**Phase 7 Status**: ✅ **COMPLETE**
|
||||
**Implementation Date**: January 18, 2025
|
||||
**Total Implementation Time**: 7 phases completed
|
||||
@@ -1,358 +0,0 @@
|
||||
# GitHub Project Board Setup & Automation Guide
|
||||
|
||||
This guide will help you set up the project board and automation for CaddyProxyManager+.
|
||||
|
||||
## 🎯 Quick Start (5 Minutes)
|
||||
|
||||
### Step 1: Create the Project Board
|
||||
|
||||
1. Go to https://github.com/Wikid82/CaddyProxyManagerPlus/projects
|
||||
2. Click **"New project"**
|
||||
3. Choose **"Board"** view
|
||||
4. Name it: `CaddyProxyManager+ Development`
|
||||
5. Click **"Create"**
|
||||
|
||||
### Step 2: Configure Project Columns
|
||||
|
||||
The new GitHub Projects automatically creates columns. Add these views/columns:
|
||||
|
||||
#### Recommended Column Setup:
|
||||
1. **📋 Backlog** - Issues that are planned but not started
|
||||
2. **🏗️ Alpha** - Core foundation features (v0.1)
|
||||
3. **🔐 Beta - Security** - Authentication, WAF, CrowdSec features
|
||||
4. **📊 Beta - Monitoring** - Logging, dashboards, analytics
|
||||
5. **🎨 Beta - UX** - UI improvements and user experience
|
||||
6. **🚧 In Progress** - Currently being worked on
|
||||
7. **👀 Review** - Ready for code review
|
||||
8. **✅ Done** - Completed issues
|
||||
|
||||
### Step 3: Get Your Project Number
|
||||
|
||||
After creating the project, your URL will look like:
|
||||
```
|
||||
https://github.com/users/Wikid82/projects/1
|
||||
```
|
||||
|
||||
The number at the end (e.g., `1`) is your **PROJECT_NUMBER**.
|
||||
|
||||
### Step 4: Update the Automation Workflow
|
||||
|
||||
1. Open `.github/workflows/auto-add-to-project.yml`
|
||||
2. Replace `YOUR_PROJECT_NUMBER` with your actual project number:
|
||||
```yaml
|
||||
project-url: https://github.com/users/Wikid82/projects/1
|
||||
```
|
||||
3. Commit and push the change
|
||||
|
||||
### Step 5: Create Labels
|
||||
|
||||
1. Go to: https://github.com/Wikid82/CaddyProxyManagerPlus/actions
|
||||
2. Find the workflow: **"Create Project Labels"**
|
||||
3. Click **"Run workflow"** > **"Run workflow"**
|
||||
4. Wait ~30 seconds - this creates all 27 labels automatically!
|
||||
|
||||
### Step 6: Test the Automation
|
||||
|
||||
1. Create a test issue with title: `[ALPHA] Test Issue`
|
||||
2. Watch it automatically:
|
||||
- Get labeled with `alpha`
|
||||
- Get added to your project board
|
||||
- Appear in the correct column
|
||||
|
||||
---
|
||||
|
||||
## 🔧 How the Automation Works
|
||||
|
||||
### Workflow 1: `auto-add-to-project.yml`
|
||||
**Triggers**: When an issue or PR is opened/reopened
|
||||
**Action**: Automatically adds it to your project board
|
||||
|
||||
### Workflow 2: `auto-label-issues.yml`
|
||||
**Triggers**: When an issue is opened or edited
|
||||
**Action**: Scans title and body for keywords and adds labels
|
||||
|
||||
**Auto-labeling Examples:**
|
||||
- Title contains `[critical]` → Adds `critical` label
|
||||
- Body contains `crowdsec` → Adds `crowdsec` label
|
||||
- Title contains `[alpha]` → Adds `alpha` label
|
||||
|
||||
### Workflow 3: `create-labels.yml`
|
||||
**Triggers**: Manual only
|
||||
**Action**: Creates all project labels with proper colors and descriptions
|
||||
|
||||
---
|
||||
|
||||
## 📝 Using the Issue Templates
|
||||
|
||||
We've created 4 specialized issue templates:
|
||||
|
||||
### 1. 🏗️ Alpha Feature (`alpha-feature.yml`)
|
||||
For core foundation features (Issues #1-10 in planning doc)
|
||||
- Automatically tagged with `alpha` and `feature`
|
||||
- Includes priority selector
|
||||
- Has task checklist format
|
||||
|
||||
### 2. 🔐 Beta Security Feature (`beta-security-feature.yml`)
|
||||
For authentication, WAF, CrowdSec, etc. (Issues #11-22)
|
||||
- Automatically tagged with `beta`, `feature`, `security`
|
||||
- Includes threat model section
|
||||
- Security testing plan included
|
||||
|
||||
### 3. 📊 Beta Monitoring Feature (`beta-monitoring-feature.yml`)
|
||||
For logging, dashboards, analytics (Issues #23-27)
|
||||
- Automatically tagged with `beta`, `feature`, `monitoring`
|
||||
- Includes metrics planning
|
||||
- UI/UX considerations section
|
||||
|
||||
### 4. ⚙️ General Feature (`general-feature.yml`)
|
||||
For any other feature request
|
||||
- Flexible milestone selection
|
||||
- Problem/solution format
|
||||
- User story section
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Label System
|
||||
|
||||
### Priority Labels (Required for all issues)
|
||||
- 🔴 **critical** - Must have, blocks other work
|
||||
- 🟠 **high** - Important, should be included
|
||||
- 🟡 **medium** - Nice to have, can be deferred
|
||||
- 🟢 **low** - Future enhancement
|
||||
|
||||
### Milestone Labels
|
||||
- 🟣 **alpha** - Foundation (v0.1)
|
||||
- 🔵 **beta** - Advanced features (v0.5)
|
||||
- 🟦 **post-beta** - Future enhancements (v1.0+)
|
||||
|
||||
### Category Labels
|
||||
- **architecture**, **backend**, **frontend**
|
||||
- **security**, **ssl**, **sso**, **waf**
|
||||
- **caddy**, **crowdsec**
|
||||
- **database**, **ui**, **deployment**
|
||||
- **monitoring**, **documentation**, **testing**
|
||||
- **performance**, **community**
|
||||
- **plus** (premium features), **enterprise**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Creating Issues from Planning Doc
|
||||
|
||||
### Method 1: Manual Creation (Recommended for control)
|
||||
|
||||
For each issue in `PROJECT_PLANNING.md`:
|
||||
|
||||
1. Click **"New Issue"**
|
||||
2. Select the appropriate template
|
||||
3. Copy content from planning doc
|
||||
4. Set priority from the planning doc
|
||||
5. Create the issue
|
||||
|
||||
The automation will:
|
||||
- ✅ Auto-label based on title keywords
|
||||
- ✅ Add to project board
|
||||
- ✅ Place in appropriate column (if configured)
|
||||
|
||||
### Method 2: Bulk Creation Script (Advanced)
|
||||
|
||||
You can create a script to bulk-import issues. Here's a sample using GitHub CLI:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# install: brew install gh
|
||||
# auth: gh auth login
|
||||
|
||||
# Example: Create Issue #1
|
||||
gh issue create \
|
||||
--title "[ALPHA] Project Architecture & Tech Stack Selection" \
|
||||
--label "alpha,critical,architecture" \
|
||||
--body-file issue_templates/issue_01.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Suggested Workflow
|
||||
|
||||
### For Project Maintainers:
|
||||
|
||||
1. **Review Planning Doc**: `PROJECT_PLANNING.md`
|
||||
2. **Create Alpha Issues First**: Issues #1-10
|
||||
3. **Prioritize in Project Board**: Drag to order
|
||||
4. **Assign to Milestones**: Create GitHub milestones
|
||||
5. **Start Development**: Pick from top of Alpha column
|
||||
6. **Move Cards**: As work progresses, move across columns
|
||||
7. **Create Beta Issues**: Once alpha is stable
|
||||
|
||||
### For Contributors:
|
||||
|
||||
1. **Browse Project Board**: See what needs work
|
||||
2. **Pick an Issue**: Comment "I'd like to work on this"
|
||||
3. **Get Assigned**: Maintainer assigns you
|
||||
4. **Submit PR**: Link to the issue
|
||||
5. **Auto-closes**: PR merge auto-closes the issue
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Required Permissions
|
||||
|
||||
The GitHub Actions workflows require these permissions:
|
||||
|
||||
- ✅ **`issues: write`** - To add labels (already included)
|
||||
- ✅ **`GITHUB_TOKEN`** - Automatically provided (already configured)
|
||||
- ⚠️ **Project Board Access** - Ensure Actions can access projects
|
||||
|
||||
### To verify project access:
|
||||
|
||||
1. Go to project settings
|
||||
2. Under "Manage access"
|
||||
3. Ensure "GitHub Actions" has write access
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Advanced: Custom Automations
|
||||
|
||||
### Auto-move to "In Progress"
|
||||
|
||||
Add this to your project board automation (in project settings):
|
||||
|
||||
**When**: Issue is assigned
|
||||
**Then**: Move to "🚧 In Progress"
|
||||
|
||||
### Auto-move to "Review"
|
||||
|
||||
**When**: PR is opened and linked to issue
|
||||
**Then**: Move issue to "👀 Review"
|
||||
|
||||
### Auto-move to "Done"
|
||||
|
||||
**When**: PR is merged
|
||||
**Then**: Move issue to "✅ Done"
|
||||
|
||||
### Auto-assign by label
|
||||
|
||||
**When**: Issue has label `critical`
|
||||
**Then**: Assign to @Wikid82
|
||||
|
||||
---
|
||||
|
||||
## 📋 Creating Your First Issues
|
||||
|
||||
Here's a suggested order to create issues from the planning doc:
|
||||
|
||||
### Week 1 - Foundation (Create these first):
|
||||
- [ ] Issue #1: Project Architecture & Tech Stack Selection
|
||||
- [ ] Issue #2: Caddy Integration & Configuration Management
|
||||
- [ ] Issue #3: Database Schema & Models
|
||||
|
||||
### Week 2 - Core UI:
|
||||
- [ ] Issue #4: Basic Web UI Foundation
|
||||
- [ ] Issue #5: Proxy Host Management (Core Feature)
|
||||
|
||||
### Week 3 - HTTPS & Security:
|
||||
- [ ] Issue #6: Automatic HTTPS & Certificate Management
|
||||
- [ ] Issue #7: User Authentication & Authorization
|
||||
|
||||
### Week 4 - Operations:
|
||||
- [ ] Issue #8: Basic Access Logging
|
||||
- [ ] Issue #9: Settings & Configuration UI
|
||||
- [ ] Issue #10: Docker & Deployment Configuration
|
||||
|
||||
**Then**: Alpha release! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Project Board Views
|
||||
|
||||
Create multiple views for different perspectives:
|
||||
|
||||
### View 1: Kanban (Default)
|
||||
All issues in status columns
|
||||
|
||||
### View 2: Priority Matrix
|
||||
Group by: Priority
|
||||
Sort by: Created date
|
||||
|
||||
### View 3: By Category
|
||||
Group by: Labels (alpha, beta, etc.)
|
||||
Filter: Not done
|
||||
|
||||
### View 4: This Sprint
|
||||
Filter: Milestone = Current Sprint
|
||||
Sort by: Priority
|
||||
|
||||
---
|
||||
|
||||
## 📱 Mobile & Desktop
|
||||
|
||||
The project board works great on:
|
||||
- 💻 GitHub Desktop
|
||||
- 📱 GitHub Mobile App
|
||||
- 🌐 Web interface
|
||||
|
||||
You can triage issues from anywhere!
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Issue doesn't get labeled automatically
|
||||
- Check title has bracketed keywords: `[ALPHA]`, `[CRITICAL]`
|
||||
- Check workflow logs: Actions > Auto-label Issues
|
||||
- Manually add labels - that's fine too!
|
||||
|
||||
### Issue doesn't appear on project board
|
||||
- Check the workflow ran: Actions > Auto-add issues
|
||||
- Verify your project URL in the workflow file
|
||||
- Manually add to project from issue sidebar
|
||||
|
||||
### Labels not created
|
||||
- Run the "Create Project Labels" workflow manually
|
||||
- Check you have admin permissions
|
||||
- Create labels manually from Issues > Labels
|
||||
|
||||
### Workflow permissions error
|
||||
- Go to Settings > Actions > General
|
||||
- Under "Workflow permissions"
|
||||
- Select "Read and write permissions"
|
||||
- Save
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Learning Resources
|
||||
|
||||
- [GitHub Projects Docs](https://docs.github.com/en/issues/planning-and-tracking-with-projects)
|
||||
- [GitHub Actions Docs](https://docs.github.com/en/actions)
|
||||
- [Issue Templates](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Checklist
|
||||
|
||||
Before starting development, ensure:
|
||||
|
||||
- [ ] Project board created
|
||||
- [ ] Project URL updated in workflow file
|
||||
- [ ] Labels created (run the workflow)
|
||||
- [ ] Issue templates tested
|
||||
- [ ] First test issue created successfully
|
||||
- [ ] Issue auto-labeled correctly
|
||||
- [ ] Issue appeared on project board
|
||||
- [ ] Column automation configured
|
||||
- [ ] Team members invited to project
|
||||
- [ ] Alpha milestone issues created (Issues #1-10)
|
||||
|
||||
---
|
||||
|
||||
## 🎉 You're Ready!
|
||||
|
||||
Your automated project management is set up! Every issue will now:
|
||||
1. ✅ Automatically get labeled
|
||||
2. ✅ Automatically added to project board
|
||||
3. ✅ Move through columns as work progresses
|
||||
4. ✅ Have structured templates for consistency
|
||||
|
||||
Focus on building awesome features - let automation handle the busywork! 🚀
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Open an issue or discussion! The automation will handle it 😉
|
||||
-1149
File diff suppressed because it is too large
Load Diff
@@ -1,403 +0,0 @@
|
||||
# Caddy Proxy Manager+ (CPMP)
|
||||
|
||||
**Make your websites easy to reach!** 🚀
|
||||
|
||||
This app helps you manage multiple websites and apps from one simple dashboard. Think of it like a **traffic director** for your internet services - it makes sure people get to the right place when they visit your websites.
|
||||
|
||||
**No coding required!** Just point, click, and you're done. ✨
|
||||
|
||||
[](LICENSE)
|
||||
[](https://go.dev/)
|
||||
[](https://react.dev/)
|
||||
|
||||
---
|
||||
|
||||
## 🤔 What Does This Do?
|
||||
|
||||
**Simple Explanation:**
|
||||
Imagine you have 5 different apps running on different computers in your house. Instead of remembering 5 complicated addresses, you can use one simple address like `myapps.com`, and this tool figures out where to send people based on what they're looking for.
|
||||
|
||||
**Real-World Example:**
|
||||
- Someone types: `blog.mysite.com` → Goes to your blog server
|
||||
- Someone types: `shop.mysite.com` → Goes to your online shop server
|
||||
- All managed from one beautiful dashboard!
|
||||
|
||||
---
|
||||
|
||||
## ✨ What Can It Do?
|
||||
|
||||
- **🎨 Beautiful Dark Interface** - Easy on the eyes, works on phones and computers
|
||||
- **🔄 Manage Multiple Websites** - Add, edit, or remove websites with a few clicks
|
||||
- **🖥️ Connect Different Servers** - Works with servers anywhere (your closet, the cloud, anywhere!)
|
||||
- **📥 Import Old Settings** - Already using Caddy? Bring your old setup right in
|
||||
- **🔍 Test Before You Save** - Check if servers are reachable before going live
|
||||
- **💾 Saves Everything Safely** - Your settings are stored securely
|
||||
- **🔐 Secure Your Sites** - Add that green lock icon (HTTPS) to your websites
|
||||
- **🌐 Works with Live Updates** - Perfect for chat apps and real-time features
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Links
|
||||
|
||||
- 🏠 [**Start Here**](docs/getting-started.md) - Your first setup in 5 minutes
|
||||
- 📚 [**All Documentation**](docs/index.md) - Find everything you need
|
||||
- 📥 [**Import Guide**](docs/import-guide.md) - Bring in your existing setup
|
||||
- 🐛 [**Report Problems**](https://github.com/Wikid82/CaddyProxyManagerPlus/issues) - We'll help!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 The Super Easy Way to Start
|
||||
|
||||
**Want to skip all the technical stuff?** Use Docker! (It's like a magic app installer)
|
||||
|
||||
### Step 1: Get Docker
|
||||
Don't have Docker? [Download it here](https://docs.docker.com/get-docker/) - it's free!
|
||||
|
||||
### Step 2: Run One Command
|
||||
Open your terminal and paste this:
|
||||
|
||||
**Real-World Example:**
|
||||
```bash
|
||||
docker run -d \
|
||||
-p 8080:8080 \
|
||||
-v caddy_data:/app/data \
|
||||
--name caddy-proxy-manager \
|
||||
ghcr.io/wikid82/cpmp:latest
|
||||
```
|
||||
|
||||
|
||||
### Step 3: Open Your Browser
|
||||
Go to: **http://localhost:8080**
|
||||
|
||||
**That's it!** 🎉 You're ready to start adding your websites!
|
||||
|
||||
> 💡 **Tip:** Not sure what a terminal is? On Windows, search for "Command Prompt". On Mac, search for "Terminal".
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ The Developer Way (If You Like Code)
|
||||
|
||||
Want to tinker with the app or help make it better? Here's how:
|
||||
|
||||
-### What You Need First:
|
||||
- **Go 1.24+** - [Get it here](https://go.dev/dl/) (the "engine" that runs the app)
|
||||
- **Node.js 20+** - [Get it here](https://nodejs.org/) (helps build the pretty interface)
|
||||
|
||||
### Getting It Running:
|
||||
|
||||
1. **Download the app**
|
||||
```bash
|
||||
git clone https://github.com/Wikid82/CaddyProxyManagerPlus.git
|
||||
cd CaddyProxyManagerPlus
|
||||
```
|
||||
|
||||
2. **Start the "brain" (backend)**
|
||||
```bash
|
||||
cd backend
|
||||
go mod download # Gets the tools it needs
|
||||
go run ./cmd/seed/main.go # Adds example data
|
||||
go run ./cmd/api/main.go # Starts the engine
|
||||
```
|
||||
|
||||
3. **Start the "face" (frontend)** - Open a NEW terminal window
|
||||
```bash
|
||||
cd frontend
|
||||
npm install # Gets the tools it needs
|
||||
npm run dev # Shows you the interface
|
||||
```
|
||||
|
||||
4. **See it work!**
|
||||
- Main app: http://localhost:3001
|
||||
- Backend: http://localhost:8080
|
||||
|
||||
### Quick Docker Way (Developers Too!)
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Opens at http://localhost:3001
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ How It's Built (For Curious Minds)
|
||||
|
||||
**Don't worry if these words sound fancy - you don't need to know them to use the app!**
|
||||
|
||||
### The "Backend" (The Smart Part)
|
||||
- **Go** - A fast programming language (like the app's brain)
|
||||
- **Gin** - Helps handle web requests quickly
|
||||
- **SQLite** - A tiny database (like a filing cabinet for your settings)
|
||||
|
||||
### The "Frontend" (The Pretty Part)
|
||||
- **React** - Makes the buttons and forms look nice
|
||||
- **TypeScript** - Keeps the code organized
|
||||
- **TailwindCSS** - Makes everything pretty with dark mode
|
||||
|
||||
### Where Things Live
|
||||
|
||||
```
|
||||
CaddyProxyManagerPlus/
|
||||
├── backend/ ← The "brain" (handles your requests)
|
||||
│ ├── cmd/ ← Starter programs
|
||||
│ ├── internal/ ← The actual code
|
||||
│ └── data/ ← Where your settings are saved
|
||||
├── frontend/ ← The "face" (what you see and click)
|
||||
│ ├── src/ ← The code for buttons and pages
|
||||
│ └── coverage/ ← Test results (proves it works!)
|
||||
└── docs/ ← Help guides (including this one!)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Making Changes to the App (For Developers)
|
||||
|
||||
Want to add your own features or fix bugs? Here's how to work on the code:
|
||||
|
||||
### Working on the Backend (The Brain)
|
||||
|
||||
1. **Get the tools it needs**
|
||||
```bash
|
||||
cd backend
|
||||
go mod download
|
||||
```
|
||||
|
||||
2. **Set up the database** (adds example data to play with)
|
||||
```bash
|
||||
go run ./cmd/seed/main.go
|
||||
```
|
||||
|
||||
3. **Make sure it works** (runs tests)
|
||||
```bash
|
||||
go test ./... -v
|
||||
```
|
||||
|
||||
4. **Start it up**
|
||||
```bash
|
||||
go run ./cmd/api/main.go
|
||||
```
|
||||
|
||||
Now the backend is running at `http://localhost:8080`
|
||||
|
||||
### Working on the Frontend (The Face)
|
||||
|
||||
1. **Get the tools it needs**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Make sure it works** (runs tests)
|
||||
```bash
|
||||
npm test # Keeps checking as you code
|
||||
npm run test:ui # Pretty visual test results
|
||||
npm run test:coverage # Shows what's tested
|
||||
```
|
||||
|
||||
3. **Start it up**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Now the frontend is running at `http://localhost:3001`
|
||||
|
||||
### Custom Settings (Optional)
|
||||
|
||||
Want to change ports or locations? Create these files:
|
||||
|
||||
**Backend Settings** (`backend/.env`):
|
||||
```env
|
||||
PORT=8080 # Where the backend listens
|
||||
DATABASE_PATH=./data/cpm.db # Where to save data
|
||||
LOG_LEVEL=debug # How much detail to show
|
||||
```
|
||||
|
||||
**Frontend Settings** (`frontend/.env`):
|
||||
```env
|
||||
VITE_API_URL=http://localhost:8080 # Where to find the backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 Controlling the App with Code (For Developers)
|
||||
|
||||
Want to automate things or build your own tools? The app has an API (a way for programs to talk to it).
|
||||
|
||||
**What's an API?** Think of it like a robot that can do things for you. You send it commands, and it does the work!
|
||||
|
||||
### Things the API Can Do:
|
||||
|
||||
#### Check if it's alive
|
||||
```http
|
||||
GET /api/v1/health
|
||||
```
|
||||
Like saying "Hey, are you there?"
|
||||
|
||||
#### Manage Your Websites
|
||||
```http
|
||||
GET /api/v1/proxy-hosts # Show me all websites
|
||||
POST /api/v1/proxy-hosts # Add a new website
|
||||
GET /api/v1/proxy-hosts/:uuid # Show me one website
|
||||
PUT /api/v1/proxy-hosts/:uuid # Change a website
|
||||
DELETE /api/v1/proxy-hosts/:uuid # Remove a website
|
||||
```
|
||||
|
||||
#### Manage Your Servers
|
||||
```http
|
||||
GET /api/v1/remote-servers # Show me all servers
|
||||
POST /api/v1/remote-servers # Add a new server
|
||||
GET /api/v1/remote-servers/:uuid # Show me one server
|
||||
PUT /api/v1/remote-servers/:uuid # Change a server
|
||||
DELETE /api/v1/remote-servers/:uuid # Remove a server
|
||||
POST /api/v1/remote-servers/:uuid/test # Is this server reachable?
|
||||
```
|
||||
|
||||
#### Import Old Files
|
||||
```http
|
||||
GET /api/v1/import/status # How's the import going?
|
||||
GET /api/v1/import/preview # Show me what will import
|
||||
POST /api/v1/import/upload # Start importing a file
|
||||
POST /api/v1/import/commit # Finish the import
|
||||
DELETE /api/v1/import/cancel # Cancel the import
|
||||
```
|
||||
|
||||
**Want more details and examples?** Check out the [complete API guide](docs/api.md)!
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Making Sure It Works (Testing)
|
||||
|
||||
**What's testing?** It's like double-checking your homework. We run automatic checks to make sure everything works before releasing updates!
|
||||
|
||||
### Checking the Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go test ./... -v # Check everything
|
||||
go test ./internal/api/handlers/... # Just check specific parts
|
||||
go test -cover ./... # Check and show what's covered
|
||||
```
|
||||
|
||||
**Results**: ✅ 6 tests passing (all working!)
|
||||
|
||||
### Checking the Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm test # Keep checking as you work
|
||||
npm run test:coverage # Show me what's tested
|
||||
npm run test:ui # Pretty visual results
|
||||
```
|
||||
|
||||
**Results**: ✅ 24 tests passing (~70% of code checked)
|
||||
- Layout: 100% ✅ (fully tested)
|
||||
- Import Table: 90% ✅ (almost fully tested)
|
||||
- Forms: ~60% ✅ (mostly tested)
|
||||
|
||||
**What does this mean for you?** The app is reliable! We've tested it thoroughly so you don't have to worry.
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Where Your Settings Are Saved
|
||||
|
||||
**What's a database?** Think of it as a super organized filing cabinet where the app remembers all your settings!
|
||||
|
||||
The app saves:
|
||||
|
||||
- **Your Websites** - All the sites you've set up
|
||||
- **Your Servers** - The computers you've connected
|
||||
- **Your Caddy Files** - Original configuration files (if you imported any)
|
||||
- **Security Stuff** - SSL certificates and who can access what
|
||||
- **App Settings** - Your preferences and customizations
|
||||
- **Import History** - What you've imported and when
|
||||
|
||||
**Want the technical details?** Check out the [database guide](docs/database-schema.md).
|
||||
|
||||
**Good news**: It's all saved in one tiny file, and you can back it up easily!
|
||||
|
||||
---
|
||||
|
||||
## 📥 Bringing In Your Old Caddy Files
|
||||
|
||||
Already using Caddy and have configuration files? No problem! You can import them:
|
||||
|
||||
**Super Simple Steps:**
|
||||
|
||||
1. **Click "Import"** in the app
|
||||
2. **Upload your file** (or just paste the text)
|
||||
3. **Look at what it found** - the app shows you what it understood
|
||||
4. **Fix any conflicts** - if something already exists, choose what to do
|
||||
5. **Click "Import"** - done!
|
||||
|
||||
**It's drag-and-drop easy!** The app figures out what everything means.
|
||||
|
||||
**Need help?** Read the [step-by-step import guide](docs/import-guide.md) with pictures and examples!
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Helpful Links
|
||||
|
||||
- **📋 What We're Working On**: https://github.com/users/Wikid82/projects/7
|
||||
- **🐛 Found a Problem?**: https://github.com/Wikid82/CaddyProxyManagerPlus/issues
|
||||
- **💬 Questions?**: https://github.com/Wikid82/CaddyProxyManagerPlus/discussions
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Want to Help Make This Better?
|
||||
|
||||
**We'd love your help!** Whether you can code or not, you can contribute:
|
||||
|
||||
**Ways You Can Help:**
|
||||
- 🐛 Report bugs (things that don't work)
|
||||
- 💡 Suggest new features (ideas for improvements)
|
||||
- 📝 Improve documentation (make guides clearer)
|
||||
- 🔧 Fix issues (if you know how to code)
|
||||
- ⭐ Star the project (shows you like it!)
|
||||
|
||||
**If You Want to Add Code:**
|
||||
|
||||
1. **Make your own copy** (click "Fork" on GitHub)
|
||||
2. **Make your changes** in a new branch
|
||||
3. **Test your changes** to make sure nothing breaks
|
||||
4. **Send us your changes** (create a "Pull Request")
|
||||
|
||||
**Don't worry if you're new!** We'll help you through the process. Check out our [Contributing Guide](CONTRIBUTING.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## 📄 Legal Stuff (License)
|
||||
|
||||
This project is **free to use**! It's under the MIT License, which basically means:
|
||||
- ✅ You can use it for free
|
||||
- ✅ You can change it
|
||||
- ✅ You can use it for your business
|
||||
- ✅ You can share it
|
||||
|
||||
See the [LICENSE](LICENSE) file for the formal details.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Special Thanks
|
||||
|
||||
- Inspired by [Nginx Proxy Manager](https://nginxproxymanager.com/) (similar tool, different approach)
|
||||
- Built with [Caddy Server](https://caddyserver.com/) (the power behind the scenes)
|
||||
- Made beautiful with [TailwindCSS](https://tailwindcss.com/) (the styling magic)
|
||||
|
||||
---
|
||||
|
||||
## 💬 Questions?
|
||||
|
||||
**Stuck?** Don't be shy!
|
||||
- 📖 Check the [documentation](docs/index.md)
|
||||
- 💬 Ask in [Discussions](https://github.com/Wikid82/CaddyProxyManagerPlus/discussions)
|
||||
- 🐛 Open an [Issue](https://github.com/Wikid82/CaddyProxyManagerPlus/issues) if something's broken
|
||||
|
||||
**We're here to help!** Everyone was a beginner once. 🌟
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Version 0.1.0</strong><br>
|
||||
<em>Built with ❤️ by <a href="https://github.com/Wikid82">@Wikid82</a></em><br>
|
||||
<em>Made for humans, not just techies!</em>
|
||||
</p>
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
# Versioning Guide
|
||||
|
||||
## Semantic Versioning
|
||||
|
||||
CaddyProxyManager+ follows [Semantic Versioning 2.0.0](https://semver.org/):
|
||||
|
||||
- **MAJOR.MINOR.PATCH** (e.g., `1.2.3`)
|
||||
- **MAJOR**: Incompatible API changes
|
||||
- **MINOR**: New functionality (backward compatible)
|
||||
- **PATCH**: Bug fixes (backward compatible)
|
||||
|
||||
### Pre-release Identifiers
|
||||
- `alpha`: Early development, unstable
|
||||
- `beta`: Feature complete, testing phase
|
||||
- `rc` (release candidate): Final testing before release
|
||||
|
||||
Example: `0.1.0-alpha`, `1.0.0-beta.1`, `2.0.0-rc.2`
|
||||
|
||||
## Creating a Release
|
||||
|
||||
### Automated Release Process
|
||||
|
||||
1. **Update version** in `.version` file:
|
||||
```bash
|
||||
echo "1.0.0" > .version
|
||||
```
|
||||
|
||||
2. **Commit version bump**:
|
||||
```bash
|
||||
git add .version
|
||||
git commit -m "chore: bump version to 1.0.0"
|
||||
```
|
||||
|
||||
3. **Create and push tag**:
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
4. **GitHub Actions automatically**:
|
||||
- Creates GitHub Release with changelog
|
||||
- Builds multi-arch Docker images (amd64, arm64)
|
||||
- Publishes to GitHub Container Registry with tags:
|
||||
- `v1.0.0` (exact version)
|
||||
- `1.0` (minor version)
|
||||
- `1` (major version)
|
||||
- `latest` (for non-prerelease on main branch)
|
||||
|
||||
## Container Image Tags
|
||||
|
||||
### Available Tags
|
||||
|
||||
- **`latest`**: Latest stable release (main branch)
|
||||
- **`development`**: Latest development build (development branch)
|
||||
- **`v1.2.3`**: Specific version tag
|
||||
- **`1.2`**: Latest patch for minor version
|
||||
- **`1`**: Latest minor for major version
|
||||
- **`main-<sha>`**: Commit-specific build from main
|
||||
- **`development-<sha>`**: Commit-specific build from development
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```bash
|
||||
# Use latest stable release
|
||||
docker pull ghcr.io/wikid82/cpmp:latest
|
||||
|
||||
# Use specific version
|
||||
docker pull ghcr.io/wikid82/cpmp:v1.0.0
|
||||
|
||||
# Use development builds
|
||||
docker pull ghcr.io/wikid82/cpmp:development
|
||||
|
||||
# Use specific commit
|
||||
docker pull ghcr.io/wikid82/cpmp:main-abc123
|
||||
```
|
||||
|
||||
## Version Information
|
||||
|
||||
### Runtime Version Endpoint
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/v1/health
|
||||
```
|
||||
|
||||
Response includes:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "caddy-proxy-manager-plus",
|
||||
"version": "1.0.0",
|
||||
"git_commit": "abc1234567890def",
|
||||
"build_date": "2025-11-17T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Container Image Labels
|
||||
|
||||
View version metadata:
|
||||
```bash
|
||||
docker inspect ghcr.io/wikid82/cpmp:latest \
|
||||
--format='{{json .Config.Labels}}' | jq
|
||||
```
|
||||
|
||||
Returns OCI-compliant labels:
|
||||
- `org.opencontainers.image.version`
|
||||
- `org.opencontainers.image.created`
|
||||
- `org.opencontainers.image.revision`
|
||||
- `org.opencontainers.image.source`
|
||||
|
||||
## Development Builds
|
||||
|
||||
Local builds default to `version=dev`:
|
||||
```bash
|
||||
docker build -t cpmp:dev .
|
||||
```
|
||||
|
||||
Build with custom version:
|
||||
```bash
|
||||
docker build \
|
||||
--build-arg VERSION=1.2.3 \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg VCS_REF=$(git rev-parse HEAD) \
|
||||
-t caddyproxymanagerplus:1.2.3 .
|
||||
```
|
||||
|
||||
## Changelog Generation
|
||||
|
||||
The release workflow automatically generates changelogs from commit messages. Use conventional commit format:
|
||||
|
||||
- `feat:` New features
|
||||
- `fix:` Bug fixes
|
||||
- `docs:` Documentation changes
|
||||
- `chore:` Maintenance tasks
|
||||
- `refactor:` Code refactoring
|
||||
- `test:` Test updates
|
||||
- `ci:` CI/CD changes
|
||||
|
||||
Example:
|
||||
```bash
|
||||
git commit -m "feat: add TLS certificate management"
|
||||
git commit -m "fix: correct proxy timeout handling"
|
||||
```
|
||||
@@ -1,161 +0,0 @@
|
||||
# Automated Semantic Versioning - Implementation Summary
|
||||
|
||||
## Overview
|
||||
Added comprehensive automated semantic versioning to CaddyProxyManager+ with version injection into container images, runtime version endpoints, and automated release workflows.
|
||||
|
||||
## Components Implemented
|
||||
|
||||
### 1. Dockerfile Version Injection
|
||||
**File**: `Dockerfile`
|
||||
- Added build arguments: `VERSION`, `BUILD_DATE`, `VCS_REF`
|
||||
- Backend builder injects version info via Go ldflags during compilation
|
||||
- Final image includes OCI-compliant labels for version metadata
|
||||
- Version defaults to `dev` for local builds
|
||||
|
||||
### 2. Runtime Version Package
|
||||
**File**: `backend/internal/version/version.go`
|
||||
- Added `GitCommit` and `BuildDate` variables (injected via ldflags)
|
||||
- Added `Full()` function returning complete version string
|
||||
- Version information available at runtime via `/api/v1/health` endpoint
|
||||
|
||||
### 3. Health Endpoint Enhancement
|
||||
**File**: `backend/internal/api/handlers/health_handler.go`
|
||||
- Extended to expose version metadata:
|
||||
- `version`: Semantic version (e.g., "1.0.0")
|
||||
- `git_commit`: Git commit SHA
|
||||
- `build_date`: Build timestamp
|
||||
|
||||
### 4. Docker Publishing Workflow
|
||||
**File**: `.github/workflows/docker-publish.yml`
|
||||
- Added `workflow_call` trigger for reusability
|
||||
- Uses `docker/metadata-action` for automated tag generation
|
||||
- Tag strategy:
|
||||
- `latest` for main branch
|
||||
- `development` for development branch
|
||||
- `v1.2.3`, `1.2`, `1` for semantic version tags
|
||||
- `{branch}-{sha}` for commit-specific builds
|
||||
- Passes version metadata as build args
|
||||
|
||||
### 5. Release Workflow
|
||||
**File**: `.github/workflows/release.yml`
|
||||
- Triggered on `v*.*.*` tags
|
||||
- Automatically generates changelog from commit messages
|
||||
- Creates GitHub Release (marks pre-releases for alpha/beta/rc)
|
||||
- Calls docker-publish workflow to build and publish images
|
||||
|
||||
### 6. Release Helper Script
|
||||
**File**: `scripts/release.sh`
|
||||
- Interactive script for creating releases
|
||||
- Validates semantic version format
|
||||
- Updates `.version` file
|
||||
- Creates annotated git tag
|
||||
- Pushes to remote and triggers workflows
|
||||
- Safety checks: uncommitted changes, duplicate tags
|
||||
|
||||
### 7. Version File
|
||||
**File**: `.version`
|
||||
- Single source of truth for current version
|
||||
- Current: `0.1.0-alpha`
|
||||
- Used by release script and Makefile
|
||||
|
||||
### 8. Documentation
|
||||
**File**: `VERSION.md`
|
||||
- Complete versioning guide
|
||||
- Release process documentation
|
||||
- Container image tag reference
|
||||
- Examples for all version query methods
|
||||
|
||||
### 9. Build System Updates
|
||||
**File**: `Makefile`
|
||||
- Added `docker-build-versioned`: Builds with version from `.version` file
|
||||
- Added `release`: Interactive release creation
|
||||
- Updated help text
|
||||
|
||||
**File**: `.gitignore`
|
||||
- Added `CHANGELOG.txt` to ignored files
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Release
|
||||
```bash
|
||||
# Interactive release
|
||||
make release
|
||||
|
||||
# Manual release
|
||||
echo "1.0.0" > .version
|
||||
git add .version
|
||||
git commit -m "chore: bump version to 1.0.0"
|
||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||
git push origin main
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
### Building with Version
|
||||
```bash
|
||||
# Using Makefile (reads from .version)
|
||||
make docker-build-versioned
|
||||
|
||||
# Manual with custom version
|
||||
docker build \
|
||||
--build-arg VERSION=1.2.3 \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg VCS_REF=$(git rev-parse HEAD) \
|
||||
-t cpmp:1.2.3 .
|
||||
```
|
||||
|
||||
### Querying Version at Runtime
|
||||
```bash
|
||||
# Health endpoint includes version
|
||||
curl http://localhost:8080/api/v1/health
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "CPMP",
|
||||
"version": "1.0.0",
|
||||
"git_commit": "abc1234567890def",
|
||||
"build_date": "2025-11-17T12:34:56Z"
|
||||
}
|
||||
|
||||
# Container image labels
|
||||
docker inspect ghcr.io/wikid82/cpmp:latest \
|
||||
--format='{{json .Config.Labels}}' | jq
|
||||
```
|
||||
|
||||
## Automated Workflows
|
||||
|
||||
### On Tag Push (v1.2.3)
|
||||
1. Release workflow creates GitHub Release with changelog
|
||||
2. Docker publish workflow builds multi-arch images (amd64, arm64)
|
||||
3. Images tagged: `v1.2.3`, `1.2`, `1`, `latest` (if main)
|
||||
4. Published to GitHub Container Registry
|
||||
|
||||
### On Branch Push
|
||||
1. Docker publish workflow builds images
|
||||
2. Images tagged: `development` or `main-{sha}`
|
||||
3. Published to GHCR (not for PRs)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Traceability**: Every container image traceable to exact git commit
|
||||
2. **Automation**: Zero-touch release process after tag push
|
||||
3. **Flexibility**: Multiple tag strategies (latest, semver, commit-specific)
|
||||
4. **Standards**: OCI-compliant image labels
|
||||
5. **Runtime Discovery**: Version queryable via API endpoint
|
||||
6. **User Experience**: Clear version information for support/debugging
|
||||
|
||||
## Testing
|
||||
|
||||
Version injection tested and working:
|
||||
- ✅ Go binary builds with ldflags injection
|
||||
- ✅ Health endpoint returns version info
|
||||
- ✅ Dockerfile ARGs properly scoped
|
||||
- ✅ OCI labels properly set
|
||||
- ✅ Release script validates input
|
||||
- ✅ Workflows configured correctly
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test full release workflow with actual tag push
|
||||
2. Consider adding `/api/v1/version` dedicated endpoint
|
||||
3. Display version in frontend UI footer
|
||||
4. Add version to error reports/logs
|
||||
5. Document version strategy in contributor guide
|
||||
@@ -1,5 +0,0 @@
|
||||
CPM_ENV=development
|
||||
CPM_HTTP_PORT=8080
|
||||
CPM_DB_PATH=./data/cpm.db
|
||||
CPM_CADDY_ADMIN_API=http://localhost:2019
|
||||
CPM_CADDY_CONFIG_DIR=./data/caddy
|
||||
@@ -1,19 +0,0 @@
|
||||
# Backend Service
|
||||
|
||||
This folder contains the Go API for CaddyProxyManager+.
|
||||
|
||||
## Prerequisites
|
||||
- Go 1.24+
|
||||
|
||||
## Getting started
|
||||
```bash
|
||||
cp .env.example .env # optional
|
||||
cd backend
|
||||
go run ./cmd/api
|
||||
```
|
||||
|
||||
## Tests
|
||||
```bash
|
||||
cd backend
|
||||
go test ./...
|
||||
```
|
||||
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/routes"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/database"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/server"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Printf("starting %s backend on version %s", version.Name, version.Full())
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Connect(cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("connect database: %v", err)
|
||||
}
|
||||
|
||||
router := server.NewRouter(cfg.FrontendDir)
|
||||
|
||||
// Pass config to routes for auth service and certificate service
|
||||
if err := routes.Register(router, db, cfg); err != nil {
|
||||
log.Fatalf("register routes: %v", err)
|
||||
}
|
||||
|
||||
// Register import handler with config dependencies
|
||||
routes.RegisterImportHandler(router, db, cfg.CaddyBinary, cfg.ImportDir)
|
||||
|
||||
// Check for mounted Caddyfile on startup
|
||||
if err := handlers.CheckMountedImport(db, cfg.ImportCaddyfile, cfg.CaddyBinary, cfg.ImportDir); err != nil {
|
||||
log.Printf("WARNING: failed to process mounted Caddyfile: %v", err)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf(":%s", cfg.HTTPPort)
|
||||
log.Printf("starting %s backend on %s", version.Name, addr)
|
||||
|
||||
if err := router.Run(addr); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Connect to database
|
||||
db, err := gorm.Open(sqlite.Open("./data/cpm.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
// Auto migrate
|
||||
if err := db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.ProxyHost{},
|
||||
&models.CaddyConfig{},
|
||||
&models.RemoteServer{},
|
||||
&models.SSLCertificate{},
|
||||
&models.AccessList{},
|
||||
&models.Setting{},
|
||||
&models.ImportSession{},
|
||||
); err != nil {
|
||||
log.Fatal("Failed to migrate database:", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Database migrated successfully")
|
||||
|
||||
// Seed Remote Servers
|
||||
remoteServers := []models.RemoteServer{
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Local Docker Registry",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 5000,
|
||||
Scheme: "http",
|
||||
Description: "Local Docker container registry",
|
||||
Enabled: true,
|
||||
Reachable: false,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Development API Server",
|
||||
Provider: "generic",
|
||||
Host: "192.168.1.100",
|
||||
Port: 8080,
|
||||
Scheme: "http",
|
||||
Description: "Main development API backend",
|
||||
Enabled: true,
|
||||
Reachable: false,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Staging Web App",
|
||||
Provider: "vm",
|
||||
Host: "staging.internal",
|
||||
Port: 3000,
|
||||
Scheme: "http",
|
||||
Description: "Staging environment web application",
|
||||
Enabled: true,
|
||||
Reachable: false,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Database Admin",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 8081,
|
||||
Scheme: "http",
|
||||
Description: "PhpMyAdmin or similar DB management tool",
|
||||
Enabled: false,
|
||||
Reachable: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, server := range remoteServers {
|
||||
result := db.Where("host = ? AND port = ?", server.Host, server.Port).FirstOrCreate(&server)
|
||||
if result.Error != nil {
|
||||
log.Printf("Failed to seed remote server %s: %v", server.Name, result.Error)
|
||||
} else if result.RowsAffected > 0 {
|
||||
fmt.Printf("✓ Created remote server: %s (%s:%d)\n", server.Name, server.Host, server.Port)
|
||||
} else {
|
||||
fmt.Printf(" Remote server already exists: %s\n", server.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Seed Proxy Hosts
|
||||
proxyHosts := []models.ProxyHost{
|
||||
{
|
||||
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",
|
||||
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",
|
||||
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_names = ?", host.DomainNames).FirstOrCreate(&host)
|
||||
if result.Error != nil {
|
||||
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.DomainNames, host.ForwardScheme, host.ForwardHost, host.ForwardPort)
|
||||
} else {
|
||||
fmt.Printf(" Proxy host already exists: %s\n", host.DomainNames)
|
||||
}
|
||||
}
|
||||
|
||||
// Seed Settings
|
||||
settings := []models.Setting{
|
||||
{
|
||||
Key: "app_name",
|
||||
Value: "Caddy Proxy Manager+",
|
||||
Type: "string",
|
||||
Category: "general",
|
||||
},
|
||||
{
|
||||
Key: "default_scheme",
|
||||
Value: "http",
|
||||
Type: "string",
|
||||
Category: "general",
|
||||
},
|
||||
{
|
||||
Key: "enable_ssl_by_default",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Category: "security",
|
||||
},
|
||||
}
|
||||
|
||||
for _, setting := range settings {
|
||||
result := db.Where("key = ?", setting.Key).FirstOrCreate(&setting)
|
||||
if result.Error != nil {
|
||||
log.Printf("Failed to seed setting %s: %v", setting.Key, result.Error)
|
||||
} else if result.RowsAffected > 0 {
|
||||
fmt.Printf("✓ Created setting: %s = %s\n", setting.Key, setting.Value)
|
||||
} else {
|
||||
fmt.Printf(" Setting already exists: %s\n", setting.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// Seed default admin user (for future authentication)
|
||||
user := models.User{
|
||||
UUID: uuid.NewString(),
|
||||
Email: "admin@localhost",
|
||||
Name: "Administrator",
|
||||
PasswordHash: "$2a$10$example_hashed_password", // This would be properly hashed in production
|
||||
Role: "admin",
|
||||
Enabled: true,
|
||||
}
|
||||
result := db.Where("email = ?", user.Email).FirstOrCreate(&user)
|
||||
if result.Error != nil {
|
||||
log.Printf("Failed to seed user: %v", result.Error)
|
||||
} else if result.RowsAffected > 0 {
|
||||
fmt.Printf("✓ Created default user: %s\n", user.Email)
|
||||
} else {
|
||||
fmt.Printf(" User already exists: %s\n", user.Email)
|
||||
}
|
||||
|
||||
fmt.Println("\n✓ Database seeding completed successfully!")
|
||||
fmt.Println(" You can now start the application and see sample data.")
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
module github.com/Wikid82/CaddyProxyManagerPlus/backend
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/crypto v0.45.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@@ -1,99 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{authService: authService}
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
c.SetCookie("auth_token", token, 3600*24, "/", "", false, true) // Secure should be true in prod
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.Register(req.Email, req.Password, req.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, user)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
c.SetCookie("auth_token", "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Me(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
role, _ := c.Get("role")
|
||||
c.JSON(http.StatusOK, gin.H{"user_id": userID, "role": role})
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ChangePassword(c *gin.Context) {
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.authService.ChangePassword(userID.(uint), req.OldPassword, req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"})
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
type CertificateHandler struct {
|
||||
service *services.CertificateService
|
||||
}
|
||||
|
||||
func NewCertificateHandler(service *services.CertificateService) *CertificateHandler {
|
||||
return &CertificateHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *CertificateHandler) List(c *gin.Context) {
|
||||
certs, err := h.service.ListCertificates()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, certs)
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupTestDB() *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect to test database")
|
||||
}
|
||||
|
||||
// Auto migrate
|
||||
db.AutoMigrate(
|
||||
&models.ProxyHost{},
|
||||
&models.Location{},
|
||||
&models.RemoteServer{},
|
||||
&models.ImportSession{},
|
||||
)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_List(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Server",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(server)
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test List
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/remote-servers", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var servers []models.RemoteServer
|
||||
err := json.Unmarshal(w.Body.Bytes(), &servers)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, servers, 1)
|
||||
assert.Equal(t, "Test Server", servers[0].Name)
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_Create(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test Create
|
||||
serverData := map[string]interface{}{
|
||||
"name": "New Server",
|
||||
"provider": "generic",
|
||||
"host": "192.168.1.100",
|
||||
"port": 3000,
|
||||
"enabled": true,
|
||||
}
|
||||
body, _ := json.Marshal(serverData)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/remote-servers", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var server models.RemoteServer
|
||||
err := json.Unmarshal(w.Body.Bytes(), &server)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "New Server", server.Name)
|
||||
assert.NotEmpty(t, server.UUID)
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_TestConnection(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Server",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 99999, // Invalid port to test failure
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(server)
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test connection
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/remote-servers/"+server.UUID+"/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result["reachable"].(bool))
|
||||
assert.NotEmpty(t, result["error"])
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_Get(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Server",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(server)
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test Get
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/remote-servers/"+server.UUID, nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var fetched models.RemoteServer
|
||||
err := json.Unmarshal(w.Body.Bytes(), &fetched)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, server.UUID, fetched.UUID)
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_Update(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Server",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(server)
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test Update
|
||||
updateData := map[string]interface{}{
|
||||
"name": "Updated Server",
|
||||
"provider": "generic",
|
||||
"host": "10.0.0.1",
|
||||
"port": 9000,
|
||||
"enabled": false,
|
||||
}
|
||||
body, _ := json.Marshal(updateData)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/v1/remote-servers/"+server.UUID, bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var updated models.RemoteServer
|
||||
err := json.Unmarshal(w.Body.Bytes(), &updated)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Updated Server", updated.Name)
|
||||
assert.Equal(t, "generic", updated.Provider)
|
||||
assert.False(t, updated.Enabled)
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_Delete(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
// Create test server
|
||||
server := &models.RemoteServer{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Server",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(server)
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test Delete
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/remote-servers/"+server.UUID, nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNoContent, w.Code)
|
||||
|
||||
// Verify Delete
|
||||
w2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("GET", "/api/v1/remote-servers/"+server.UUID, nil)
|
||||
router.ServeHTTP(w2, req2)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w2.Code)
|
||||
}
|
||||
|
||||
func TestProxyHostHandler_List(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
// Create test proxy host
|
||||
host := &models.ProxyHost{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Host",
|
||||
DomainNames: "test.local",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 3000,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(host)
|
||||
|
||||
handler := handlers.NewProxyHostHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test List
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/proxy-hosts", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var hosts []models.ProxyHost
|
||||
err := json.Unmarshal(w.Body.Bytes(), &hosts)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, hosts, 1)
|
||||
assert.Equal(t, "Test Host", hosts[0].Name)
|
||||
}
|
||||
|
||||
func TestProxyHostHandler_Create(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
handler := handlers.NewProxyHostHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Test Create
|
||||
hostData := map[string]interface{}{
|
||||
"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)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/proxy-hosts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var host models.ProxyHost
|
||||
err := json.Unmarshal(w.Body.Bytes(), &host)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "New Host", host.Name)
|
||||
assert.Equal(t, "new.local", host.DomainNames)
|
||||
assert.NotEmpty(t, host.UUID)
|
||||
}
|
||||
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.GET("/health", handlers.HealthHandler)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var result map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", result["status"])
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HealthHandler responds with basic service metadata for uptime checks.
|
||||
func HealthHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"service": version.Name,
|
||||
"version": version.Version,
|
||||
"git_commit": version.GitCommit,
|
||||
"build_time": version.BuildTime,
|
||||
})
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/caddy"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
// ImportHandler handles Caddyfile import operations.
|
||||
type ImportHandler struct {
|
||||
db *gorm.DB
|
||||
proxyHostSvc *services.ProxyHostService
|
||||
importerservice *caddy.Importer
|
||||
importDir string
|
||||
}
|
||||
|
||||
// NewImportHandler creates a new import handler.
|
||||
func NewImportHandler(db *gorm.DB, caddyBinary, importDir string) *ImportHandler {
|
||||
return &ImportHandler{
|
||||
db: db,
|
||||
proxyHostSvc: services.NewProxyHostService(db),
|
||||
importerservice: caddy.NewImporter(caddyBinary),
|
||||
importDir: importDir,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers import-related routes.
|
||||
func (h *ImportHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
router.GET("/import/status", h.GetStatus)
|
||||
router.GET("/import/preview", h.GetPreview)
|
||||
router.POST("/import/upload", h.Upload)
|
||||
router.POST("/import/commit", h.Commit)
|
||||
router.DELETE("/import/cancel", h.Cancel)
|
||||
}
|
||||
|
||||
// GetStatus returns current import session status.
|
||||
func (h *ImportHandler) GetStatus(c *gin.Context) {
|
||||
var session models.ImportSession
|
||||
err := h.db.Where("status IN ?", []string{"pending", "reviewing"}).
|
||||
Order("created_at DESC").
|
||||
First(&session).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusOK, gin.H{"has_pending": false})
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"has_pending": true,
|
||||
"session": session,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPreview returns parsed hosts and conflicts for review.
|
||||
func (h *ImportHandler) GetPreview(c *gin.Context) {
|
||||
var session models.ImportSession
|
||||
err := h.db.Where("status IN ?", []string{"pending", "reviewing"}).
|
||||
Order("created_at DESC").
|
||||
First(&session).Error
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no pending import"})
|
||||
return
|
||||
}
|
||||
|
||||
var result caddy.ImportResult
|
||||
if err := json.Unmarshal([]byte(session.ParsedData), &result); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse import data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update status to reviewing
|
||||
session.Status = "reviewing"
|
||||
h.db.Save(&session)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Upload handles manual Caddyfile upload or paste.
|
||||
func (h *ImportHandler) Upload(c *gin.Context) {
|
||||
var req struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create temporary file
|
||||
tempPath := filepath.Join(h.importDir, fmt.Sprintf("upload-%s.caddyfile", uuid.NewString()))
|
||||
if err := os.MkdirAll(h.importDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create import directory"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tempPath, []byte(req.Content), 0644); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write upload"})
|
||||
return
|
||||
}
|
||||
|
||||
// Process the uploaded file
|
||||
if err := h.processImport(tempPath, req.Filename); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "upload processed, ready for review"})
|
||||
}
|
||||
|
||||
// Commit finalizes the import with user's conflict resolutions.
|
||||
func (h *ImportHandler) Commit(c *gin.Context) {
|
||||
var req struct {
|
||||
SessionUUID string `json:"session_uuid" binding:"required"`
|
||||
Resolutions map[string]string `json:"resolutions"` // domain -> action (skip, rename, merge)
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var session models.ImportSession
|
||||
if err := h.db.Where("uuid = ? AND status = ?", req.SessionUUID, "reviewing").First(&session).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "session not found or not in reviewing state"})
|
||||
return
|
||||
}
|
||||
|
||||
var result caddy.ImportResult
|
||||
if err := json.Unmarshal([]byte(session.ParsedData), &result); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse import data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert parsed hosts to ProxyHost models
|
||||
proxyHosts := caddy.ConvertToProxyHosts(result.Hosts)
|
||||
|
||||
created := 0
|
||||
skipped := 0
|
||||
errors := []string{}
|
||||
|
||||
for _, host := range proxyHosts {
|
||||
action := req.Resolutions[host.DomainNames]
|
||||
|
||||
if action == "skip" {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
if action == "rename" {
|
||||
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.DomainNames, err.Error()))
|
||||
} else {
|
||||
created++
|
||||
}
|
||||
}
|
||||
|
||||
// Mark session as committed
|
||||
now := time.Now()
|
||||
session.Status = "committed"
|
||||
session.CommittedAt = &now
|
||||
session.UserResolutions = string(mustMarshal(req.Resolutions))
|
||||
h.db.Save(&session)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"created": created,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
})
|
||||
}
|
||||
|
||||
// Cancel discards a pending import session.
|
||||
func (h *ImportHandler) Cancel(c *gin.Context) {
|
||||
sessionUUID := c.Query("session_uuid")
|
||||
if sessionUUID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "session_uuid required"})
|
||||
return
|
||||
}
|
||||
|
||||
var session models.ImportSession
|
||||
if err := h.db.Where("uuid = ?", sessionUUID).First(&session).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "session not found"})
|
||||
return
|
||||
}
|
||||
|
||||
session.Status = "rejected"
|
||||
h.db.Save(&session)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "import cancelled"})
|
||||
}
|
||||
|
||||
// processImport handles the import logic for both mounted and uploaded files.
|
||||
func (h *ImportHandler) processImport(caddyfilePath, originalName string) error {
|
||||
// Validate Caddy binary
|
||||
if err := h.importerservice.ValidateCaddyBinary(); err != nil {
|
||||
return fmt.Errorf("caddy binary not available: %w", err)
|
||||
}
|
||||
|
||||
// Parse and extract hosts
|
||||
result, err := h.importerservice.ImportFile(caddyfilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import failed: %w", err)
|
||||
}
|
||||
|
||||
// Check for conflicts with existing hosts
|
||||
existingHosts, _ := h.proxyHostSvc.List()
|
||||
existingDomains := make(map[string]bool)
|
||||
for _, host := range existingHosts {
|
||||
existingDomains[host.DomainNames] = true
|
||||
}
|
||||
|
||||
for _, parsed := range result.Hosts {
|
||||
if existingDomains[parsed.DomainNames] {
|
||||
result.Conflicts = append(result.Conflicts,
|
||||
fmt.Sprintf("Domain '%s' already exists in CPM+", parsed.DomainNames))
|
||||
}
|
||||
}
|
||||
|
||||
// Create import session
|
||||
session := models.ImportSession{
|
||||
UUID: uuid.NewString(),
|
||||
SourceFile: originalName,
|
||||
Status: "pending",
|
||||
ParsedData: string(mustMarshal(result)),
|
||||
ConflictReport: string(mustMarshal(result.Conflicts)),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&session).Error; err != nil {
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
// Backup original file
|
||||
if _, err := caddy.BackupCaddyfile(caddyfilePath, filepath.Join(h.importDir, "backups")); err != nil {
|
||||
// Non-fatal, log and continue
|
||||
fmt.Printf("Warning: failed to backup Caddyfile: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckMountedImport checks for mounted Caddyfile on startup.
|
||||
func CheckMountedImport(db *gorm.DB, mountPath, caddyBinary, importDir string) error {
|
||||
if _, err := os.Stat(mountPath); os.IsNotExist(err) {
|
||||
return nil // No mounted file, skip
|
||||
}
|
||||
|
||||
// Check if already processed
|
||||
var count int64
|
||||
db.Model(&models.ImportSession{}).Where("source_file = ? AND status IN ?",
|
||||
mountPath, []string{"pending", "reviewing", "committed"}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
return nil // Already processed
|
||||
}
|
||||
|
||||
handler := NewImportHandler(db, caddyBinary, importDir)
|
||||
return handler.processImport(mountPath, mountPath)
|
||||
}
|
||||
|
||||
func mustMarshal(v interface{}) []byte {
|
||||
b, _ := json.Marshal(v)
|
||||
return b
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
// ProxyHostHandler handles CRUD operations for proxy hosts.
|
||||
type ProxyHostHandler struct {
|
||||
service *services.ProxyHostService
|
||||
}
|
||||
|
||||
// NewProxyHostHandler creates a new proxy host handler.
|
||||
func NewProxyHostHandler(db *gorm.DB) *ProxyHostHandler {
|
||||
return &ProxyHostHandler{
|
||||
service: services.NewProxyHostService(db),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers proxy host routes.
|
||||
func (h *ProxyHostHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
router.GET("/proxy-hosts", h.List)
|
||||
router.POST("/proxy-hosts", h.Create)
|
||||
router.GET("/proxy-hosts/:uuid", h.Get)
|
||||
router.PUT("/proxy-hosts/:uuid", h.Update)
|
||||
router.DELETE("/proxy-hosts/:uuid", h.Delete)
|
||||
}
|
||||
|
||||
// List retrieves all proxy hosts.
|
||||
func (h *ProxyHostHandler) List(c *gin.Context) {
|
||||
hosts, err := h.service.List()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, hosts)
|
||||
}
|
||||
|
||||
// Create creates a new proxy host.
|
||||
func (h *ProxyHostHandler) Create(c *gin.Context) {
|
||||
var host models.ProxyHost
|
||||
if err := c.ShouldBindJSON(&host); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, host)
|
||||
}
|
||||
|
||||
// Get retrieves a proxy host by UUID.
|
||||
func (h *ProxyHostHandler) Get(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
host, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "proxy host not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, host)
|
||||
}
|
||||
|
||||
// Update updates an existing proxy host.
|
||||
func (h *ProxyHostHandler) Update(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
host, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "proxy host not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(host); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Update(host); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, host)
|
||||
}
|
||||
|
||||
// Delete removes a proxy host.
|
||||
func (h *ProxyHostHandler) Delete(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
host, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "proxy host not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Delete(host.ID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "proxy host deleted"})
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}))
|
||||
|
||||
h := NewProxyHostHandler(db)
|
||||
r := gin.New()
|
||||
api := r.Group("/api/v1")
|
||||
h.RegisterRoutes(api)
|
||||
|
||||
return r, db
|
||||
}
|
||||
|
||||
func TestProxyHostLifecycle(t *testing.T) {
|
||||
router, _ := setupTestRouter(t)
|
||||
|
||||
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")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusCreated, resp.Code)
|
||||
|
||||
var created models.ProxyHost
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &created))
|
||||
require.Equal(t, "media.example.com", created.DomainNames)
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts", nil)
|
||||
listResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(listResp, listReq)
|
||||
require.Equal(t, http.StatusOK, listResp.Code)
|
||||
|
||||
var hosts []models.ProxyHost
|
||||
require.NoError(t, json.Unmarshal(listResp.Body.Bytes(), &hosts))
|
||||
require.Len(t, hosts, 1)
|
||||
|
||||
// Get by ID
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts/"+created.UUID, nil)
|
||||
getResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(getResp, getReq)
|
||||
require.Equal(t, http.StatusOK, getResp.Code)
|
||||
|
||||
var fetched models.ProxyHost
|
||||
require.NoError(t, json.Unmarshal(getResp.Body.Bytes(), &fetched))
|
||||
require.Equal(t, created.UUID, fetched.UUID)
|
||||
|
||||
// Update
|
||||
updateBody := `{"name":"Media Updated","domain_names":"media.example.com","forward_scheme":"http","forward_host":"media","forward_port":32400,"enabled":false}`
|
||||
updateReq := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+created.UUID, strings.NewReader(updateBody))
|
||||
updateReq.Header.Set("Content-Type", "application/json")
|
||||
updateResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(updateResp, updateReq)
|
||||
require.Equal(t, http.StatusOK, updateResp.Code)
|
||||
|
||||
var updated models.ProxyHost
|
||||
require.NoError(t, json.Unmarshal(updateResp.Body.Bytes(), &updated))
|
||||
require.Equal(t, "Media Updated", updated.Name)
|
||||
require.False(t, updated.Enabled)
|
||||
|
||||
// Delete
|
||||
delReq := httptest.NewRequest(http.MethodDelete, "/api/v1/proxy-hosts/"+created.UUID, nil)
|
||||
delResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(delResp, delReq)
|
||||
require.Equal(t, http.StatusOK, delResp.Code)
|
||||
|
||||
// Verify Delete
|
||||
getReq2 := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts/"+created.UUID, nil)
|
||||
getResp2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(getResp2, getReq2)
|
||||
require.Equal(t, http.StatusNotFound, getResp2.Code)
|
||||
}
|
||||
|
||||
func TestProxyHostErrors(t *testing.T) {
|
||||
router, _ := setupTestRouter(t)
|
||||
|
||||
// Get non-existent
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts/non-existent-uuid", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
|
||||
// Update non-existent
|
||||
updateBody := `{"name":"Media Updated"}`
|
||||
updateReq := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/non-existent-uuid", strings.NewReader(updateBody))
|
||||
updateReq.Header.Set("Content-Type", "application/json")
|
||||
updateResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(updateResp, updateReq)
|
||||
require.Equal(t, http.StatusNotFound, updateResp.Code)
|
||||
|
||||
// Delete non-existent
|
||||
delReq := httptest.NewRequest(http.MethodDelete, "/api/v1/proxy-hosts/non-existent-uuid", nil)
|
||||
delResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(delResp, delReq)
|
||||
require.Equal(t, http.StatusNotFound, delResp.Code)
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
// RemoteServerHandler handles HTTP requests for remote server management.
|
||||
type RemoteServerHandler struct {
|
||||
service *services.RemoteServerService
|
||||
}
|
||||
|
||||
// NewRemoteServerHandler creates a new remote server handler.
|
||||
func NewRemoteServerHandler(db *gorm.DB) *RemoteServerHandler {
|
||||
return &RemoteServerHandler{
|
||||
service: services.NewRemoteServerService(db),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers remote server routes.
|
||||
func (h *RemoteServerHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
router.GET("/remote-servers", h.List)
|
||||
router.POST("/remote-servers", h.Create)
|
||||
router.GET("/remote-servers/:uuid", h.Get)
|
||||
router.PUT("/remote-servers/:uuid", h.Update)
|
||||
router.DELETE("/remote-servers/:uuid", h.Delete)
|
||||
router.POST("/remote-servers/:uuid/test", h.TestConnection)
|
||||
}
|
||||
|
||||
// List retrieves all remote servers.
|
||||
func (h *RemoteServerHandler) List(c *gin.Context) {
|
||||
enabledOnly := c.Query("enabled") == "true"
|
||||
|
||||
servers, err := h.service.List(enabledOnly)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, servers)
|
||||
}
|
||||
|
||||
// Create creates a new remote server.
|
||||
func (h *RemoteServerHandler) Create(c *gin.Context) {
|
||||
var server models.RemoteServer
|
||||
if err := c.ShouldBindJSON(&server); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
server.UUID = uuid.NewString()
|
||||
|
||||
if err := h.service.Create(&server); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, server)
|
||||
}
|
||||
|
||||
// Get retrieves a remote server by UUID.
|
||||
func (h *RemoteServerHandler) Get(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
server, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
}
|
||||
|
||||
// Update updates an existing remote server.
|
||||
func (h *RemoteServerHandler) Update(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
server, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(server); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Update(server); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
}
|
||||
|
||||
// Delete removes a remote server.
|
||||
func (h *RemoteServerHandler) Delete(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
server, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Delete(server.ID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// TestConnection tests the TCP connection to a remote server.
|
||||
func (h *RemoteServerHandler) TestConnection(c *gin.Context) {
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
server, err := h.service.GetByUUID(uuid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Test TCP connection with 5 second timeout
|
||||
address := net.JoinHostPort(server.Host, fmt.Sprintf("%d", server.Port))
|
||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
|
||||
result := gin.H{
|
||||
"server_uuid": server.UUID,
|
||||
"address": address,
|
||||
"timestamp": time.Now().UTC(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result["reachable"] = false
|
||||
result["error"] = err.Error()
|
||||
|
||||
// Update server reachability status
|
||||
server.Reachable = false
|
||||
now := time.Now().UTC()
|
||||
server.LastChecked = &now
|
||||
h.service.Update(server)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Connection successful
|
||||
result["reachable"] = true
|
||||
result["latency_ms"] = time.Since(time.Now()).Milliseconds()
|
||||
|
||||
// Update server reachability status
|
||||
server.Reachable = true
|
||||
now := time.Now().UTC()
|
||||
server.LastChecked = &now
|
||||
h.service.Update(server)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserHandler(db *gorm.DB) *UserHandler {
|
||||
return &UserHandler{DB: db}
|
||||
}
|
||||
|
||||
func (h *UserHandler) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.GET("/setup", h.GetSetupStatus)
|
||||
r.POST("/setup", h.Setup)
|
||||
}
|
||||
|
||||
// GetSetupStatus checks if the application needs initial setup (i.e., no users exist).
|
||||
func (h *UserHandler) GetSetupStatus(c *gin.Context) {
|
||||
var count int64
|
||||
if err := h.DB.Model(&models.User{}).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check setup status"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"setupRequired": count == 0,
|
||||
})
|
||||
}
|
||||
|
||||
type SetupRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
// Setup creates the initial admin user and configures the ACME email.
|
||||
func (h *UserHandler) Setup(c *gin.Context) {
|
||||
// 1. Check if setup is allowed
|
||||
var count int64
|
||||
if err := h.DB.Model(&models.User{}).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check setup status"})
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Setup already completed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Parse request
|
||||
var req SetupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Create User
|
||||
user := models.User{
|
||||
UUID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Role: "admin",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
if err := user.SetPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Create Setting for ACME Email
|
||||
acmeEmailSetting := models.Setting{
|
||||
Key: "caddy.acme_email",
|
||||
Value: req.Email,
|
||||
Type: "string",
|
||||
Category: "caddy",
|
||||
}
|
||||
|
||||
// Transaction to ensure both succeed
|
||||
err := h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Use Save to update if exists (though it shouldn't in fresh setup) or create
|
||||
if err := tx.Where(models.Setting{Key: "caddy.acme_email"}).Assign(models.Setting{Value: req.Email}).FirstOrCreate(&acmeEmailSetting).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to complete setup: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Setup completed successfully",
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"name": user.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthMiddleware(authService *services.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
// Try cookie
|
||||
cookie, err := c.Cookie("auth_token")
|
||||
if err == nil {
|
||||
authHeader = "Bearer " + cookie
|
||||
}
|
||||
}
|
||||
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := authService.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("role", claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireRole(role string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userRole, exists := c.Get("role")
|
||||
if !exists {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
if userRole.(string) != role && userRole.(string) != "admin" {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/middleware"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
// Register wires up API routes and performs automatic migrations.
|
||||
func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error {
|
||||
// AutoMigrate all models for Issue #5 persistence layer
|
||||
if err := db.AutoMigrate(
|
||||
&models.ProxyHost{},
|
||||
&models.Location{},
|
||||
&models.CaddyConfig{},
|
||||
&models.RemoteServer{},
|
||||
&models.SSLCertificate{},
|
||||
&models.AccessList{},
|
||||
&models.User{},
|
||||
&models.Setting{},
|
||||
&models.ImportSession{},
|
||||
); err != nil {
|
||||
return fmt.Errorf("auto migrate: %w", err)
|
||||
}
|
||||
|
||||
router.GET("/api/v1/health", handlers.HealthHandler)
|
||||
|
||||
api := router.Group("/api/v1")
|
||||
|
||||
// Auth routes
|
||||
authService := services.NewAuthService(db, cfg)
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
authMiddleware := middleware.AuthMiddleware(authService)
|
||||
|
||||
api.POST("/auth/login", authHandler.Login)
|
||||
api.POST("/auth/register", authHandler.Register)
|
||||
|
||||
protected := api.Group("/")
|
||||
protected.Use(authMiddleware)
|
||||
{
|
||||
protected.POST("/auth/logout", authHandler.Logout)
|
||||
protected.GET("/auth/me", authHandler.Me)
|
||||
protected.POST("/auth/change-password", authHandler.ChangePassword)
|
||||
}
|
||||
|
||||
proxyHostHandler := handlers.NewProxyHostHandler(db)
|
||||
proxyHostHandler.RegisterRoutes(api)
|
||||
|
||||
remoteServerHandler := handlers.NewRemoteServerHandler(db)
|
||||
remoteServerHandler.RegisterRoutes(api)
|
||||
|
||||
userHandler := handlers.NewUserHandler(db)
|
||||
userHandler.RegisterRoutes(api)
|
||||
|
||||
// Certificate routes
|
||||
// Use cfg.CaddyConfigDir + "/data" for cert service
|
||||
caddyDataDir := cfg.CaddyConfigDir + "/data"
|
||||
certService := services.NewCertificateService(caddyDataDir)
|
||||
certHandler := handlers.NewCertificateHandler(certService)
|
||||
api.GET("/certificates", certHandler.List)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterImportHandler wires up import routes with config dependencies.
|
||||
func RegisterImportHandler(router *gin.Engine, db *gorm.DB, caddyBinary, importDir string) {
|
||||
importHandler := handlers.NewImportHandler(db, caddyBinary, importDir)
|
||||
api := router.Group("/api/v1")
|
||||
importHandler.RegisterRoutes(api)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client wraps the Caddy admin API.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a Caddy API client.
|
||||
func NewClient(adminAPIURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: adminAPIURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Load atomically replaces Caddy's entire configuration.
|
||||
// This is the primary method for applying configuration changes.
|
||||
func (c *Client) Load(ctx context.Context, config *Config) error {
|
||||
body, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal config: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/load", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("caddy returned status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig retrieves the current running configuration from Caddy.
|
||||
func (c *Client) GetConfig(ctx context.Context) (*Config, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/config/", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("caddy returned status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Ping checks if Caddy admin API is reachable.
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/config/", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("caddy unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("caddy returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestClient_Load_Success(t *testing.T) {
|
||||
// Mock Caddy admin API
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/load", r.URL.Path)
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL)
|
||||
config, _ := GenerateConfig([]models.ProxyHost{
|
||||
{
|
||||
UUID: "test",
|
||||
DomainNames: "test.com",
|
||||
ForwardHost: "app",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}, "/tmp/caddy-data", "admin@example.com")
|
||||
|
||||
err := client.Load(context.Background(), config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_Load_Failure(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error": "invalid config"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL)
|
||||
config := &Config{}
|
||||
|
||||
err := client.Load(context.Background(), config)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "400")
|
||||
}
|
||||
|
||||
func TestClient_GetConfig_Success(t *testing.T) {
|
||||
testConfig := &Config{
|
||||
Apps: Apps{
|
||||
HTTP: &HTTPApp{
|
||||
Servers: map[string]*Server{
|
||||
"test": {Listen: []string{":80"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/config/", r.URL.Path)
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(testConfig)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL)
|
||||
config, err := client.GetConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
}
|
||||
|
||||
func TestClient_Ping_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL)
|
||||
err := client.Ping(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_Ping_Unreachable(t *testing.T) {
|
||||
client := NewClient("http://localhost:9999")
|
||||
err := client.Ping(context.Background())
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
// GenerateConfig creates a Caddy JSON configuration from proxy hosts.
|
||||
// This is the core transformation layer from our database model to Caddy config.
|
||||
func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string) (*Config, error) {
|
||||
config := &Config{
|
||||
Apps: Apps{
|
||||
HTTP: &HTTPApp{
|
||||
Servers: map[string]*Server{},
|
||||
},
|
||||
},
|
||||
Storage: Storage{
|
||||
System: "file_system",
|
||||
Root: storageDir,
|
||||
},
|
||||
}
|
||||
|
||||
if acmeEmail != "" {
|
||||
config.Apps.TLS = &TLSApp{
|
||||
Automation: &AutomationConfig{
|
||||
Policies: []*AutomationPolicy{
|
||||
{
|
||||
IssuersRaw: []interface{}{
|
||||
map[string]interface{}{
|
||||
"module": "acme",
|
||||
"email": acmeEmail,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"module": "zerossl",
|
||||
"email": acmeEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
routes := make([]*Route, 0)
|
||||
|
||||
for _, host := range hosts {
|
||||
if !host.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
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: domains},
|
||||
},
|
||||
Handle: mainHandlers,
|
||||
Terminal: true,
|
||||
}
|
||||
|
||||
routes = append(routes, route)
|
||||
}
|
||||
|
||||
config.Apps.HTTP.Servers["cpm_server"] = &Server{
|
||||
Listen: []string{":80", ":443"},
|
||||
Routes: routes,
|
||||
AutoHTTPS: &AutoHTTPSConfig{
|
||||
Disable: false,
|
||||
DisableRedir: false,
|
||||
},
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestGenerateConfig_Empty(t *testing.T) {
|
||||
config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
require.Empty(t, config.Apps.HTTP.Servers)
|
||||
}
|
||||
|
||||
func TestGenerateConfig_SingleHost(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "test-uuid",
|
||||
Name: "Media",
|
||||
DomainNames: "media.example.com",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "media",
|
||||
ForwardPort: 32400,
|
||||
SSLForced: true,
|
||||
WebsocketSupport: false,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
require.Len(t, config.Apps.HTTP.Servers, 1)
|
||||
|
||||
server := config.Apps.HTTP.Servers["cpm_server"]
|
||||
require.NotNil(t, server)
|
||||
require.Contains(t, server.Listen, ":80")
|
||||
require.Contains(t, server.Listen, ":443")
|
||||
require.Len(t, server.Routes, 1)
|
||||
|
||||
route := server.Routes[0]
|
||||
require.Len(t, route.Match, 1)
|
||||
require.Equal(t, []string{"media.example.com"}, route.Match[0].Host)
|
||||
require.Len(t, route.Handle, 1)
|
||||
require.True(t, route.Terminal)
|
||||
|
||||
handler := route.Handle[0]
|
||||
require.Equal(t, "reverse_proxy", handler["handler"])
|
||||
}
|
||||
|
||||
func TestGenerateConfig_MultipleHosts(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "uuid-1",
|
||||
DomainNames: "site1.example.com",
|
||||
ForwardHost: "app1",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
UUID: "uuid-2",
|
||||
DomainNames: "site2.example.com",
|
||||
ForwardHost: "app2",
|
||||
ForwardPort: 8081,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, config.Apps.HTTP.Servers["cpm_server"].Routes, 2)
|
||||
}
|
||||
|
||||
func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "uuid-ws",
|
||||
DomainNames: "ws.example.com",
|
||||
ForwardHost: "wsapp",
|
||||
ForwardPort: 3000,
|
||||
WebsocketSupport: true,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
route := config.Apps.HTTP.Servers["cpm_server"].Routes[0]
|
||||
handler := route.Handle[0]
|
||||
|
||||
// Check WebSocket headers are present
|
||||
require.NotNil(t, handler["headers"])
|
||||
}
|
||||
|
||||
func TestGenerateConfig_EmptyDomain(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "bad-uuid",
|
||||
DomainNames: "",
|
||||
ForwardHost: "app",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "empty domain")
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
// CaddyConfig represents the root structure of Caddy's JSON config.
|
||||
type CaddyConfig struct {
|
||||
Apps *CaddyApps `json:"apps,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyApps contains application-specific configurations.
|
||||
type CaddyApps struct {
|
||||
HTTP *CaddyHTTP `json:"http,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyHTTP represents the HTTP app configuration.
|
||||
type CaddyHTTP struct {
|
||||
Servers map[string]*CaddyServer `json:"servers,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyServer represents a single server configuration.
|
||||
type CaddyServer struct {
|
||||
Routes []*CaddyRoute `json:"routes,omitempty"`
|
||||
TLSConnectionPolicies interface{} `json:"tls_connection_policies,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyRoute represents a single route with matchers and handlers.
|
||||
type CaddyRoute struct {
|
||||
Match []*CaddyMatcher `json:"match,omitempty"`
|
||||
Handle []*CaddyHandler `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyMatcher represents route matching criteria.
|
||||
type CaddyMatcher struct {
|
||||
Host []string `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyHandler represents a handler in the route.
|
||||
type CaddyHandler struct {
|
||||
Handler string `json:"handler"`
|
||||
Upstreams interface{} `json:"upstreams,omitempty"`
|
||||
Headers interface{} `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// ParsedHost represents a single host detected during Caddyfile import.
|
||||
type ParsedHost struct {
|
||||
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.
|
||||
type ImportResult struct {
|
||||
Hosts []ParsedHost `json:"hosts"`
|
||||
Conflicts []string `json:"conflicts"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// Importer handles Caddyfile parsing and conversion to CPM+ models.
|
||||
type Importer struct {
|
||||
caddyBinaryPath string
|
||||
}
|
||||
|
||||
// NewImporter creates a new Caddyfile importer.
|
||||
func NewImporter(binaryPath string) *Importer {
|
||||
if binaryPath == "" {
|
||||
binaryPath = "caddy" // Default to PATH
|
||||
}
|
||||
return &Importer{caddyBinaryPath: binaryPath}
|
||||
}
|
||||
|
||||
// ParseCaddyfile reads a Caddyfile and converts it to Caddy JSON.
|
||||
func (i *Importer) ParseCaddyfile(caddyfilePath string) ([]byte, error) {
|
||||
if _, err := os.Stat(caddyfilePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("caddyfile not found: %s", caddyfilePath)
|
||||
}
|
||||
|
||||
cmd := exec.Command(i.caddyBinaryPath, "adapt", "--config", caddyfilePath, "--adapter", "caddyfile")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("caddy adapt failed: %w (output: %s)", err, string(output))
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ExtractHosts parses Caddy JSON and extracts proxy host information.
|
||||
func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
|
||||
var config CaddyConfig
|
||||
if err := json.Unmarshal(caddyJSON, &config); err != nil {
|
||||
return nil, fmt.Errorf("parsing caddy json: %w", err)
|
||||
}
|
||||
|
||||
result := &ImportResult{
|
||||
Hosts: []ParsedHost{},
|
||||
Conflicts: []string{},
|
||||
Errors: []string{},
|
||||
}
|
||||
|
||||
if config.Apps == nil || config.Apps.HTTP == nil || config.Apps.HTTP.Servers == nil {
|
||||
return result, nil // Empty config
|
||||
}
|
||||
|
||||
seenDomains := make(map[string]bool)
|
||||
|
||||
for serverName, server := range config.Apps.HTTP.Servers {
|
||||
for routeIdx, route := range server.Routes {
|
||||
for _, match := range route.Match {
|
||||
for _, hostMatcher := range match.Host {
|
||||
domain := hostMatcher
|
||||
|
||||
// Check for duplicate domains
|
||||
if seenDomains[domain] {
|
||||
result.Conflicts = append(result.Conflicts,
|
||||
fmt.Sprintf("Duplicate domain detected: %s", domain))
|
||||
continue
|
||||
}
|
||||
seenDomains[domain] = true
|
||||
|
||||
// Extract reverse proxy handler
|
||||
host := ParsedHost{
|
||||
DomainNames: domain,
|
||||
SSLForced: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil,
|
||||
}
|
||||
|
||||
// Find reverse_proxy handler
|
||||
for _, handler := range route.Handle {
|
||||
if handler.Handler == "reverse_proxy" {
|
||||
upstreams, _ := handler.Upstreams.([]interface{})
|
||||
if len(upstreams) > 0 {
|
||||
if upstream, ok := upstreams[0].(map[string]interface{}); ok {
|
||||
dial, _ := upstream["dial"].(string)
|
||||
if dial != "" {
|
||||
parts := strings.Split(dial, ":")
|
||||
if len(parts) == 2 {
|
||||
host.ForwardHost = parts[0]
|
||||
fmt.Sscanf(parts[1], "%d", &host.ForwardPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for websocket support
|
||||
if headers, ok := handler.Headers.(map[string]interface{}); ok {
|
||||
if upgrade, ok := headers["Upgrade"].([]interface{}); ok {
|
||||
for _, v := range upgrade {
|
||||
if v == "websocket" {
|
||||
host.WebsocketSupport = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default scheme
|
||||
host.ForwardScheme = "http"
|
||||
if host.SSLForced {
|
||||
host.ForwardScheme = "https"
|
||||
}
|
||||
}
|
||||
|
||||
// Detect unsupported features
|
||||
if handler.Handler == "rewrite" {
|
||||
host.Warnings = append(host.Warnings, "Rewrite rules not supported - manual configuration required")
|
||||
}
|
||||
if handler.Handler == "file_server" {
|
||||
host.Warnings = append(host.Warnings, "File server directives not supported")
|
||||
}
|
||||
}
|
||||
|
||||
// Store raw JSON for this route
|
||||
routeJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"server": serverName,
|
||||
"route": routeIdx,
|
||||
"data": route,
|
||||
})
|
||||
host.RawJSON = string(routeJSON)
|
||||
|
||||
result.Hosts = append(result.Hosts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ImportFile performs complete import: parse Caddyfile and extract hosts.
|
||||
func (i *Importer) ImportFile(caddyfilePath string) (*ImportResult, error) {
|
||||
caddyJSON, err := i.ParseCaddyfile(caddyfilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.ExtractHosts(caddyJSON)
|
||||
}
|
||||
|
||||
// ConvertToProxyHosts converts parsed hosts to ProxyHost models.
|
||||
func ConvertToProxyHosts(parsedHosts []ParsedHost) []models.ProxyHost {
|
||||
hosts := make([]models.ProxyHost, 0, len(parsedHosts))
|
||||
|
||||
for _, parsed := range parsedHosts {
|
||||
if parsed.ForwardHost == "" || parsed.ForwardPort == 0 {
|
||||
continue // Skip invalid entries
|
||||
}
|
||||
|
||||
hosts = append(hosts, models.ProxyHost{
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// ValidateCaddyBinary checks if the Caddy binary is available.
|
||||
func (i *Importer) ValidateCaddyBinary() error {
|
||||
cmd := exec.Command(i.caddyBinaryPath, "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.New("caddy binary not found or not executable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupCaddyfile creates a timestamped backup of the original Caddyfile.
|
||||
func BackupCaddyfile(originalPath, backupDir string) (string, error) {
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("creating backup directory: %w", err)
|
||||
}
|
||||
|
||||
timestamp := fmt.Sprintf("%d", os.Getpid()) // Simple timestamp placeholder
|
||||
backupPath := filepath.Join(backupDir, fmt.Sprintf("Caddyfile.%s.backup", timestamp))
|
||||
|
||||
input, err := os.ReadFile(originalPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading original file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(backupPath, input, 0644); err != nil {
|
||||
return "", fmt.Errorf("writing backup: %w", err)
|
||||
}
|
||||
|
||||
return backupPath, nil
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
// Manager orchestrates Caddy configuration lifecycle: generate, validate, apply, rollback.
|
||||
type Manager struct {
|
||||
client *Client
|
||||
db *gorm.DB
|
||||
configDir string
|
||||
}
|
||||
|
||||
// NewManager creates a configuration manager.
|
||||
func NewManager(client *Client, db *gorm.DB, configDir string) *Manager {
|
||||
return &Manager{
|
||||
client: client,
|
||||
db: db,
|
||||
configDir: configDir,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyConfig generates configuration from database, validates it, applies to Caddy with rollback on failure.
|
||||
func (m *Manager) ApplyConfig(ctx context.Context) error {
|
||||
// Fetch all proxy hosts from database
|
||||
var hosts []models.ProxyHost
|
||||
if err := m.db.Find(&hosts).Error; err != nil {
|
||||
return fmt.Errorf("fetch proxy hosts: %w", err)
|
||||
}
|
||||
|
||||
// Fetch ACME email setting
|
||||
var acmeEmailSetting models.Setting
|
||||
var acmeEmail string
|
||||
if err := m.db.Where("key = ?", "caddy.acme_email").First(&acmeEmailSetting).Error; err == nil {
|
||||
acmeEmail = acmeEmailSetting.Value
|
||||
}
|
||||
|
||||
// Generate Caddy config
|
||||
config, err := GenerateConfig(hosts, filepath.Join(m.configDir, "data"), acmeEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %w", err)
|
||||
}
|
||||
|
||||
// Validate before applying
|
||||
if err := Validate(config); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Save snapshot for rollback
|
||||
if _, err := m.saveSnapshot(config); err != nil {
|
||||
return fmt.Errorf("save snapshot: %w", err)
|
||||
}
|
||||
|
||||
// Calculate config hash for audit trail
|
||||
configJSON, _ := json.Marshal(config)
|
||||
configHash := fmt.Sprintf("%x", sha256.Sum256(configJSON))
|
||||
|
||||
// Apply to Caddy
|
||||
if err := m.client.Load(ctx, config); err != nil {
|
||||
// Rollback on failure
|
||||
if rollbackErr := m.rollback(ctx); rollbackErr != nil {
|
||||
return fmt.Errorf("apply failed: %w, rollback also failed: %v", err, rollbackErr)
|
||||
}
|
||||
|
||||
// Record failed attempt
|
||||
m.recordConfigChange(configHash, false, err.Error())
|
||||
return fmt.Errorf("apply failed (rolled back): %w", err)
|
||||
}
|
||||
|
||||
// Record successful application
|
||||
m.recordConfigChange(configHash, true, "")
|
||||
|
||||
// Cleanup old snapshots (keep last 10)
|
||||
if err := m.rotateSnapshots(10); err != nil {
|
||||
// Non-fatal - log but don't fail
|
||||
fmt.Printf("warning: snapshot rotation failed: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveSnapshot stores the config to disk with timestamp.
|
||||
func (m *Manager) saveSnapshot(config *Config) (string, error) {
|
||||
timestamp := time.Now().Unix()
|
||||
filename := fmt.Sprintf("config-%d.json", timestamp)
|
||||
path := filepath.Join(m.configDir, filename)
|
||||
|
||||
configJSON, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, configJSON, 0644); err != nil {
|
||||
return "", fmt.Errorf("write snapshot: %w", err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// rollback loads the most recent snapshot from disk.
|
||||
func (m *Manager) rollback(ctx context.Context) error {
|
||||
snapshots, err := m.listSnapshots()
|
||||
if err != nil || len(snapshots) == 0 {
|
||||
return fmt.Errorf("no snapshots available for rollback")
|
||||
}
|
||||
|
||||
// Load most recent snapshot
|
||||
latestSnapshot := snapshots[len(snapshots)-1]
|
||||
configJSON, err := os.ReadFile(latestSnapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read snapshot: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := json.Unmarshal(configJSON, &config); err != nil {
|
||||
return fmt.Errorf("unmarshal snapshot: %w", err)
|
||||
}
|
||||
|
||||
// Apply the snapshot
|
||||
if err := m.client.Load(ctx, &config); err != nil {
|
||||
return fmt.Errorf("load snapshot: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listSnapshots returns all snapshot file paths sorted by modification time.
|
||||
func (m *Manager) listSnapshots() ([]string, error) {
|
||||
entries, err := os.ReadDir(m.configDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config dir: %w", err)
|
||||
}
|
||||
|
||||
var snapshots []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
snapshots = append(snapshots, filepath.Join(m.configDir, entry.Name()))
|
||||
}
|
||||
|
||||
// Sort by modification time
|
||||
sort.Slice(snapshots, func(i, j int) bool {
|
||||
infoI, _ := os.Stat(snapshots[i])
|
||||
infoJ, _ := os.Stat(snapshots[j])
|
||||
return infoI.ModTime().Before(infoJ.ModTime())
|
||||
})
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// rotateSnapshots keeps only the N most recent snapshots.
|
||||
func (m *Manager) rotateSnapshots(keep int) error {
|
||||
snapshots, err := m.listSnapshots()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(snapshots) <= keep {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete oldest snapshots
|
||||
toDelete := snapshots[:len(snapshots)-keep]
|
||||
for _, path := range toDelete {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("delete snapshot %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordConfigChange stores an audit record in the database.
|
||||
func (m *Manager) recordConfigChange(configHash string, success bool, errorMsg string) {
|
||||
record := models.CaddyConfig{
|
||||
ConfigHash: configHash,
|
||||
AppliedAt: time.Now(),
|
||||
Success: success,
|
||||
ErrorMsg: errorMsg,
|
||||
}
|
||||
|
||||
// Best effort - don't fail if audit logging fails
|
||||
m.db.Create(&record)
|
||||
}
|
||||
|
||||
// Ping checks if Caddy is reachable.
|
||||
func (m *Manager) Ping(ctx context.Context) error {
|
||||
return m.client.Ping(ctx)
|
||||
}
|
||||
|
||||
// GetCurrentConfig retrieves the running config from Caddy.
|
||||
func (m *Manager) GetCurrentConfig(ctx context.Context) (*Config, error) {
|
||||
return m.client.GetConfig(ctx)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package caddy
|
||||
|
||||
// Config represents Caddy's top-level JSON configuration structure.
|
||||
// Reference: https://caddyserver.com/docs/json/
|
||||
type Config struct {
|
||||
Apps Apps `json:"apps"`
|
||||
Storage Storage `json:"storage,omitempty"`
|
||||
}
|
||||
|
||||
// Storage configures the storage module.
|
||||
type Storage struct {
|
||||
System string `json:"module"`
|
||||
Root string `json:"root,omitempty"`
|
||||
}
|
||||
|
||||
// Apps contains all Caddy app modules.
|
||||
type Apps struct {
|
||||
HTTP *HTTPApp `json:"http,omitempty"`
|
||||
TLS *TLSApp `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPApp configures the HTTP app.
|
||||
type HTTPApp struct {
|
||||
Servers map[string]*Server `json:"servers"`
|
||||
}
|
||||
|
||||
// Server represents an HTTP server instance.
|
||||
type Server struct {
|
||||
Listen []string `json:"listen"`
|
||||
Routes []*Route `json:"routes"`
|
||||
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
|
||||
Logs *ServerLogs `json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
// AutoHTTPSConfig controls automatic HTTPS behavior.
|
||||
type AutoHTTPSConfig struct {
|
||||
Disable bool `json:"disable,omitempty"`
|
||||
DisableRedir bool `json:"disable_redirects,omitempty"`
|
||||
Skip []string `json:"skip,omitempty"`
|
||||
}
|
||||
|
||||
// ServerLogs configures access logging.
|
||||
type ServerLogs struct {
|
||||
DefaultLoggerName string `json:"default_logger_name,omitempty"`
|
||||
}
|
||||
|
||||
// Route represents an HTTP route (matcher + handlers).
|
||||
type Route struct {
|
||||
Match []Match `json:"match,omitempty"`
|
||||
Handle []Handler `json:"handle"`
|
||||
Terminal bool `json:"terminal,omitempty"`
|
||||
}
|
||||
|
||||
// Match represents a request matcher.
|
||||
type Match struct {
|
||||
Host []string `json:"host,omitempty"`
|
||||
Path []string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// Handler is the interface for all handler types.
|
||||
// Actual types will implement handler-specific fields.
|
||||
type Handler map[string]interface{}
|
||||
|
||||
// ReverseProxyHandler creates a reverse_proxy handler.
|
||||
func ReverseProxyHandler(dial string, enableWS bool) Handler {
|
||||
h := Handler{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": []map[string]interface{}{
|
||||
{"dial": dial},
|
||||
},
|
||||
}
|
||||
|
||||
if enableWS {
|
||||
// Enable WebSocket support by preserving upgrade headers
|
||||
h["headers"] = map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"set": map[string][]string{
|
||||
"Upgrade": {"{http.request.header.Upgrade}"},
|
||||
"Connection": {"{http.request.header.Connection}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// AutomationConfig controls certificate automation.
|
||||
type AutomationConfig struct {
|
||||
Policies []*AutomationPolicy `json:"policies,omitempty"`
|
||||
}
|
||||
|
||||
// AutomationPolicy defines certificate management for specific domains.
|
||||
type AutomationPolicy struct {
|
||||
Subjects []string `json:"subjects,omitempty"`
|
||||
IssuersRaw []interface{} `json:"issuers,omitempty"`
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Validate performs pre-flight validation on a Caddy config before applying it.
|
||||
func Validate(cfg *Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config cannot be nil")
|
||||
}
|
||||
|
||||
if cfg.Apps.HTTP == nil {
|
||||
return nil // Empty config is valid
|
||||
}
|
||||
|
||||
// Track seen hosts to detect duplicates
|
||||
seenHosts := make(map[string]bool)
|
||||
|
||||
for serverName, server := range cfg.Apps.HTTP.Servers {
|
||||
if len(server.Listen) == 0 {
|
||||
return fmt.Errorf("server %s has no listen addresses", serverName)
|
||||
}
|
||||
|
||||
// Validate listen addresses
|
||||
for _, addr := range server.Listen {
|
||||
if err := validateListenAddr(addr); err != nil {
|
||||
return fmt.Errorf("invalid listen address %s in server %s: %w", addr, serverName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate routes
|
||||
for i, route := range server.Routes {
|
||||
if err := validateRoute(route, seenHosts); err != nil {
|
||||
return fmt.Errorf("invalid route %d in server %s: %w", i, serverName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JSON marshalling works
|
||||
if _, err := json.Marshal(cfg); err != nil {
|
||||
return fmt.Errorf("config cannot be marshalled to JSON: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateListenAddr(addr string) error {
|
||||
// Strip network type prefix if present (tcp/, udp/)
|
||||
if idx := strings.Index(addr, "/"); idx != -1 {
|
||||
addr = addr[idx+1:]
|
||||
}
|
||||
|
||||
// Parse host:port
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid address format: %w", err)
|
||||
}
|
||||
|
||||
// Validate port
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port: %w", err)
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return fmt.Errorf("port %d out of range (1-65535)", port)
|
||||
}
|
||||
|
||||
// Validate host (allow empty for wildcard binding)
|
||||
if host != "" && net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("invalid IP address: %s", host)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRoute(route *Route, seenHosts map[string]bool) error {
|
||||
if len(route.Handle) == 0 {
|
||||
return fmt.Errorf("route has no handlers")
|
||||
}
|
||||
|
||||
// Check for duplicate host matchers
|
||||
for _, match := range route.Match {
|
||||
for _, host := range match.Host {
|
||||
if seenHosts[host] {
|
||||
return fmt.Errorf("duplicate host matcher: %s", host)
|
||||
}
|
||||
seenHosts[host] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate handlers
|
||||
for i, handler := range route.Handle {
|
||||
if err := validateHandler(handler); err != nil {
|
||||
return fmt.Errorf("invalid handler %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHandler(handler Handler) error {
|
||||
handlerType, ok := handler["handler"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("handler missing 'handler' field")
|
||||
}
|
||||
|
||||
switch handlerType {
|
||||
case "reverse_proxy":
|
||||
return validateReverseProxy(handler)
|
||||
case "file_server", "static_response":
|
||||
return nil // Accept other common handlers
|
||||
default:
|
||||
// Unknown handlers are allowed (Caddy is extensible)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateReverseProxy(handler Handler) error {
|
||||
upstreams, ok := handler["upstreams"].([]map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("reverse_proxy missing upstreams")
|
||||
}
|
||||
|
||||
if len(upstreams) == 0 {
|
||||
return fmt.Errorf("reverse_proxy has no upstreams")
|
||||
}
|
||||
|
||||
for i, upstream := range upstreams {
|
||||
dial, ok := upstream["dial"].(string)
|
||||
if !ok || dial == "" {
|
||||
return fmt.Errorf("upstream %d missing dial address", i)
|
||||
}
|
||||
|
||||
// Validate dial address format (host:port)
|
||||
if _, _, err := net.SplitHostPort(dial); err != nil {
|
||||
return fmt.Errorf("upstream %d has invalid dial address %s: %w", i, dial, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestValidate_EmptyConfig(t *testing.T) {
|
||||
config := &Config{}
|
||||
err := Validate(config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidate_ValidConfig(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "test",
|
||||
DomainNames: "test.example.com",
|
||||
ForwardHost: "10.0.1.100",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com")
|
||||
err := Validate(config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidate_DuplicateHosts(t *testing.T) {
|
||||
config := &Config{
|
||||
Apps: Apps{
|
||||
HTTP: &HTTPApp{
|
||||
Servers: map[string]*Server{
|
||||
"srv": {
|
||||
Listen: []string{":80"},
|
||||
Routes: []*Route{
|
||||
{
|
||||
Match: []Match{{Host: []string{"test.com"}}},
|
||||
Handle: []Handler{
|
||||
ReverseProxyHandler("app:8080", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
Match: []Match{{Host: []string{"test.com"}}},
|
||||
Handle: []Handler{
|
||||
ReverseProxyHandler("app2:8080", false),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := Validate(config)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "duplicate host")
|
||||
}
|
||||
|
||||
func TestValidate_NoListenAddresses(t *testing.T) {
|
||||
config := &Config{
|
||||
Apps: Apps{
|
||||
HTTP: &HTTPApp{
|
||||
Servers: map[string]*Server{
|
||||
"srv": {
|
||||
Listen: []string{},
|
||||
Routes: []*Route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := Validate(config)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no listen addresses")
|
||||
}
|
||||
|
||||
func TestValidate_InvalidPort(t *testing.T) {
|
||||
config := &Config{
|
||||
Apps: Apps{
|
||||
HTTP: &HTTPApp{
|
||||
Servers: map[string]*Server{
|
||||
"srv": {
|
||||
Listen: []string{":99999"},
|
||||
Routes: []*Route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := Validate(config)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "out of range")
|
||||
}
|
||||
|
||||
func TestValidate_NoHandlers(t *testing.T) {
|
||||
config := &Config{
|
||||
Apps: Apps{
|
||||
HTTP: &HTTPApp{
|
||||
Servers: map[string]*Server{
|
||||
"srv": {
|
||||
Listen: []string{":80"},
|
||||
Routes: []*Route{
|
||||
{
|
||||
Match: []Match{{Host: []string{"test.com"}}},
|
||||
Handle: []Handler{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := Validate(config)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no handlers")
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Config captures runtime configuration sourced from environment variables.
|
||||
type Config struct {
|
||||
Environment string
|
||||
HTTPPort string
|
||||
DatabasePath string
|
||||
FrontendDir string
|
||||
CaddyAdminAPI string
|
||||
CaddyConfigDir string
|
||||
CaddyBinary string
|
||||
ImportCaddyfile string
|
||||
ImportDir string
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
// Load reads env vars and falls back to defaults so the server can boot with zero configuration.
|
||||
func Load() (Config, error) {
|
||||
cfg := Config{
|
||||
Environment: getEnv("CPM_ENV", "development"),
|
||||
HTTPPort: getEnv("CPM_HTTP_PORT", "8080"),
|
||||
DatabasePath: getEnv("CPM_DB_PATH", filepath.Join("data", "cpm.db")),
|
||||
FrontendDir: getEnv("CPM_FRONTEND_DIR", filepath.Clean(filepath.Join("..", "frontend", "dist"))),
|
||||
CaddyAdminAPI: getEnv("CPM_CADDY_ADMIN_API", "http://localhost:2019"),
|
||||
CaddyConfigDir: getEnv("CPM_CADDY_CONFIG_DIR", filepath.Join("data", "caddy")),
|
||||
CaddyBinary: getEnv("CPM_CADDY_BINARY", "caddy"),
|
||||
ImportCaddyfile: getEnv("CPM_IMPORT_CADDYFILE", "/import/Caddyfile"),
|
||||
ImportDir: getEnv("CPM_IMPORT_DIR", filepath.Join("data", "imports")),
|
||||
JWTSecret: getEnv("CPM_JWT_SECRET", "change-me-in-production"),
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o755); err != nil {
|
||||
return Config{}, fmt.Errorf("ensure data directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cfg.CaddyConfigDir, 0o755); err != nil {
|
||||
return Config{}, fmt.Errorf("ensure caddy config directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cfg.ImportDir, 0o755); err != nil {
|
||||
return Config{}, fmt.Errorf("ensure import directory: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Connect opens a SQLite database connection.
|
||||
func Connect(dbPath string) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AccessList defines IP-based or auth-based access control rules
|
||||
// that can be applied to proxy hosts.
|
||||
type AccessList struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"` // "allow", "deny", "basic_auth", "forward_auth"
|
||||
Rules string `json:"rules" gorm:"type:text"` // JSON array of rule definitions
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CaddyConfig stores an audit trail of Caddy configuration changes.
|
||||
type CaddyConfig struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ConfigHash string `json:"config_hash" gorm:"index"`
|
||||
AppliedAt time.Time `json:"applied_at"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ImportSession tracks Caddyfile import operations with pending state
|
||||
// until user reviews and confirms via UI.
|
||||
type ImportSession struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
SourceFile string `json:"source_file"` // Path to original Caddyfile
|
||||
Status string `json:"status" gorm:"default:'pending'"` // "pending", "reviewing", "committed", "rejected", "failed"
|
||||
ParsedData string `json:"parsed_data" gorm:"type:text"` // JSON representation of detected hosts
|
||||
ConflictReport string `json:"conflict_report" gorm:"type:text"` // JSON array of conflicts
|
||||
UserResolutions string `json:"user_resolutions" gorm:"type:text"` // JSON map of conflict resolutions
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CommittedAt *time.Time `json:"committed_at,omitempty"`
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProxyHost represents a reverse proxy configuration.
|
||||
type ProxyHost struct {
|
||||
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"`
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// RemoteServer represents a known backend server that can be selected
|
||||
// when creating proxy hosts, eliminating manual IP/port entry.
|
||||
type RemoteServer struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Provider string `json:"provider"` // e.g., "docker", "vm", "cloud", "manual"
|
||||
Host string `json:"host"` // IP address or hostname
|
||||
Port int `json:"port"`
|
||||
Scheme string `json:"scheme"` // http/https
|
||||
Tags string `json:"tags"` // comma-separated tags for filtering
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
LastChecked *time.Time `json:"last_checked,omitempty"`
|
||||
Reachable bool `json:"reachable" gorm:"default:false"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Setting stores global application configuration as key-value pairs.
|
||||
// Used for system-wide preferences, feature flags, and runtime config.
|
||||
type Setting struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Key string `json:"key" gorm:"uniqueIndex"`
|
||||
Value string `json:"value" gorm:"type:text"`
|
||||
Type string `json:"type"` // "string", "int", "bool", "json"
|
||||
Category string `json:"category"` // "general", "security", "caddy", "smtp", etc.
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// SSLCertificate represents TLS certificates managed by CPM+.
|
||||
// Can be Let's Encrypt auto-generated or custom uploaded certs.
|
||||
type SSLCertificate struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"` // "letsencrypt", "custom", "self-signed"
|
||||
Domains string `json:"domains"` // comma-separated list of domains
|
||||
Certificate string `json:"certificate" gorm:"type:text"` // PEM-encoded certificate
|
||||
PrivateKey string `json:"private_key" gorm:"type:text"` // PEM-encoded private key
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
AutoRenew bool `json:"auto_renew" gorm:"default:false"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// User represents authenticated users with role-based access control.
|
||||
// Supports local auth, SSO integration planned for later phases.
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Email string `json:"email" gorm:"uniqueIndex"`
|
||||
PasswordHash string `json:"-"` // Never serialize password hash
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role" gorm:"default:'user'"` // "admin", "user", "viewer"
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
FailedLoginAttempts int `json:"-" gorm:"default:0"`
|
||||
LockedUntil *time.Time `json:"-"`
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// SetPassword hashes and sets the user's password.
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.PasswordHash = string(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword compares the provided password with the stored hash.
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// NewRouter creates a new Gin router with frontend static file serving.
|
||||
func NewRouter(frontendDir string) *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
// Serve frontend static files
|
||||
if frontendDir != "" {
|
||||
router.Static("/assets", frontendDir+"/assets")
|
||||
router.StaticFile("/", frontendDir+"/index.html")
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.File(frontendDir + "/index.html")
|
||||
})
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
config config.Config
|
||||
}
|
||||
|
||||
func NewAuthService(db *gorm.DB, cfg config.Config) *AuthService {
|
||||
return &AuthService{db: db, config: cfg}
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(email, password, name string) (*models.User, error) {
|
||||
var count int64
|
||||
s.db.Model(&models.User{}).Count(&count)
|
||||
|
||||
role := "user"
|
||||
if count == 0 {
|
||||
role = "admin" // First user is admin
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
UUID: uuid.New().String(),
|
||||
Email: email,
|
||||
Name: name,
|
||||
Role: role,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.db.Create(user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(email, password string) (string, error) {
|
||||
var user models.User
|
||||
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
return "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
if !user.Enabled {
|
||||
return "", errors.New("account disabled")
|
||||
}
|
||||
|
||||
if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) {
|
||||
return "", errors.New("account locked")
|
||||
}
|
||||
|
||||
if !user.CheckPassword(password) {
|
||||
user.FailedLoginAttempts++
|
||||
if user.FailedLoginAttempts >= 5 {
|
||||
lockTime := time.Now().Add(15 * time.Minute)
|
||||
user.LockedUntil = &lockTime
|
||||
}
|
||||
s.db.Save(&user)
|
||||
return "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Reset failed attempts
|
||||
user.FailedLoginAttempts = 0
|
||||
user.LockedUntil = nil
|
||||
now := time.Now()
|
||||
user.LastLogin = &now
|
||||
s.db.Save(&user)
|
||||
|
||||
return s.GenerateToken(&user)
|
||||
}
|
||||
|
||||
func (s *AuthService) GenerateToken(user *models.User) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
Issuer: "cpmp",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWTSecret))
|
||||
}
|
||||
|
||||
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
var user models.User
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
if !user.CheckPassword(oldPassword) {
|
||||
return errors.New("invalid current password")
|
||||
}
|
||||
|
||||
if err := user.SetPassword(newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.Save(&user).Error
|
||||
}
|
||||
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(s.config.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CertificateInfo represents parsed certificate details.
|
||||
type CertificateInfo struct {
|
||||
Domain string `json:"domain"`
|
||||
Issuer string `json:"issuer"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Status string `json:"status"` // "valid", "expiring", "expired"
|
||||
}
|
||||
|
||||
// CertificateService manages certificate retrieval and parsing.
|
||||
type CertificateService struct {
|
||||
dataDir string
|
||||
}
|
||||
|
||||
// NewCertificateService creates a new certificate service.
|
||||
func NewCertificateService(dataDir string) *CertificateService {
|
||||
return &CertificateService{
|
||||
dataDir: dataDir,
|
||||
}
|
||||
}
|
||||
|
||||
// ListCertificates scans the Caddy data directory for certificates.
|
||||
// It looks in certificates/acme-v02.api.letsencrypt.org-directory/ and others.
|
||||
func (s *CertificateService) ListCertificates() ([]CertificateInfo, error) {
|
||||
certs := []CertificateInfo{}
|
||||
certRoot := filepath.Join(s.dataDir, "certificates")
|
||||
|
||||
// Walk through the certificate directory
|
||||
err := filepath.Walk(certRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// If directory doesn't exist yet (fresh install), just return empty
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// We only care about .crt files
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".crt") {
|
||||
cert, err := s.parseCertificate(path)
|
||||
if err != nil {
|
||||
// Log error but continue scanning other certs
|
||||
fmt.Printf("failed to parse cert %s: %v\n", path, err)
|
||||
return nil
|
||||
}
|
||||
certs = append(certs, *cert)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("walk certificates: %w", err)
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
func (s *CertificateService) parseCertificate(path string) (*CertificateInfo, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode PEM block")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse certificate: %w", err)
|
||||
}
|
||||
|
||||
status := "valid"
|
||||
now := time.Now()
|
||||
if now.After(cert.NotAfter) {
|
||||
status = "expired"
|
||||
} else if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) {
|
||||
status = "expiring"
|
||||
}
|
||||
|
||||
// Domain is usually the CommonName or the first SAN
|
||||
domain := cert.Subject.CommonName
|
||||
if domain == "" && len(cert.DNSNames) > 0 {
|
||||
domain = cert.DNSNames[0]
|
||||
}
|
||||
|
||||
return &CertificateInfo{
|
||||
Domain: domain,
|
||||
Issuer: cert.Issuer.CommonName,
|
||||
ExpiresAt: cert.NotAfter,
|
||||
Status: status,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
// ProxyHostService encapsulates business logic for proxy host management.
|
||||
type ProxyHostService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewProxyHostService creates a new proxy host service.
|
||||
func NewProxyHostService(db *gorm.DB) *ProxyHostService {
|
||||
return &ProxyHostService{db: db}
|
||||
}
|
||||
|
||||
// ValidateUniqueDomain ensures no duplicate domains exist before creation/update.
|
||||
func (s *ProxyHostService) ValidateUniqueDomain(domainNames string, excludeID uint) error {
|
||||
var count int64
|
||||
query := s.db.Model(&models.ProxyHost{}).Where("domain_names = ?", domainNames)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("checking domain uniqueness: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("domain already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create validates and creates a new proxy host.
|
||||
func (s *ProxyHostService) Create(host *models.ProxyHost) error {
|
||||
if err := s.ValidateUniqueDomain(host.DomainNames, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.Create(host).Error
|
||||
}
|
||||
|
||||
// Update validates and updates an existing proxy host.
|
||||
func (s *ProxyHostService) Update(host *models.ProxyHost) error {
|
||||
if err := s.ValidateUniqueDomain(host.DomainNames, host.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.Save(host).Error
|
||||
}
|
||||
|
||||
// Delete removes a proxy host.
|
||||
func (s *ProxyHostService) Delete(id uint) error {
|
||||
return s.db.Delete(&models.ProxyHost{}, id).Error
|
||||
}
|
||||
|
||||
// GetByID retrieves a proxy host by ID.
|
||||
func (s *ProxyHostService) GetByID(id uint) (*models.ProxyHost, error) {
|
||||
var host models.ProxyHost
|
||||
if err := s.db.First(&host, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// GetByUUID finds a proxy host by UUID.
|
||||
func (s *ProxyHostService) GetByUUID(uuid string) (*models.ProxyHost, error) {
|
||||
var host models.ProxyHost
|
||||
if err := s.db.Preload("Locations").Where("uuid = ?", uuid).First(&host).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// List returns all proxy hosts.
|
||||
func (s *ProxyHostService) List() ([]models.ProxyHost, error) {
|
||||
var hosts []models.ProxyHost
|
||||
if err := s.db.Preload("Locations").Order("updated_at desc").Find(&hosts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
// RemoteServerService encapsulates business logic for remote server management.
|
||||
type RemoteServerService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewRemoteServerService creates a new remote server service.
|
||||
func NewRemoteServerService(db *gorm.DB) *RemoteServerService {
|
||||
return &RemoteServerService{db: db}
|
||||
}
|
||||
|
||||
// ValidateUniqueServer ensures no duplicate name+host+port combinations.
|
||||
func (s *RemoteServerService) ValidateUniqueServer(name, host string, port int, excludeID uint) error {
|
||||
var count int64
|
||||
query := s.db.Model(&models.RemoteServer{}).Where("name = ? OR (host = ? AND port = ?)", name, host, port)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return fmt.Errorf("checking server uniqueness: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("server with same name or host:port already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create validates and creates a new remote server.
|
||||
func (s *RemoteServerService) Create(server *models.RemoteServer) error {
|
||||
if err := s.ValidateUniqueServer(server.Name, server.Host, server.Port, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.Create(server).Error
|
||||
}
|
||||
|
||||
// Update validates and updates an existing remote server.
|
||||
func (s *RemoteServerService) Update(server *models.RemoteServer) error {
|
||||
if err := s.ValidateUniqueServer(server.Name, server.Host, server.Port, server.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.db.Save(server).Error
|
||||
}
|
||||
|
||||
// Delete removes a remote server.
|
||||
func (s *RemoteServerService) Delete(id uint) error {
|
||||
return s.db.Delete(&models.RemoteServer{}, id).Error
|
||||
}
|
||||
|
||||
// GetByID retrieves a remote server by ID.
|
||||
func (s *RemoteServerService) GetByID(id uint) (*models.RemoteServer, error) {
|
||||
var server models.RemoteServer
|
||||
if err := s.db.First(&server, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
// GetByUUID retrieves a remote server by UUID.
|
||||
func (s *RemoteServerService) GetByUUID(uuid string) (*models.RemoteServer, error) {
|
||||
var server models.RemoteServer
|
||||
if err := s.db.Where("uuid = ?", uuid).First(&server).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &server, nil
|
||||
}
|
||||
|
||||
// List retrieves all remote servers, optionally filtering by enabled status.
|
||||
func (s *RemoteServerService) List(enabledOnly bool) ([]models.RemoteServer, error) {
|
||||
var servers []models.RemoteServer
|
||||
query := s.db
|
||||
|
||||
if enabledOnly {
|
||||
query = query.Where("enabled = ?", true)
|
||||
}
|
||||
|
||||
if err := query.Order("name ASC").Find(&servers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package version
|
||||
|
||||
const (
|
||||
// Name of the application
|
||||
Name = "CPMP"
|
||||
// Version is the semantic version
|
||||
Version = "0.1.0"
|
||||
// BuildTime is set during build via ldflags
|
||||
BuildTime = "unknown"
|
||||
// GitCommit is set during build via ldflags
|
||||
GitCommit = "unknown"
|
||||
)
|
||||
|
||||
// Full returns the complete version string.
|
||||
func Full() string {
|
||||
if BuildTime != "unknown" && GitCommit != "unknown" {
|
||||
return Version + " (commit: " + GitCommit + ", built: " + BuildTime + ")"
|
||||
}
|
||||
return Version
|
||||
}
|
||||
Generated
-1655
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^4.0.10"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
-1238
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
version: '3.9'
|
||||
|
||||
# Development override - use with: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/wikid82/caddyproxymanagerplus:dev
|
||||
# Development: expose Caddy admin API externally for debugging
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
- "8080:8080"
|
||||
- "2019:2019" # Caddy admin API (dev only)
|
||||
environment:
|
||||
- CPM_ENV=development
|
||||
- CPM_HTTP_PORT=8080
|
||||
- CPM_DB_PATH=/app/data/cpm.db
|
||||
- CPM_FRONTEND_DIR=/app/frontend/dist
|
||||
- CPM_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CPM_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
@@ -1,40 +0,0 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: cpmp:local
|
||||
container_name: caddyproxymanagerplus-local
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80" # HTTP (Caddy proxy)
|
||||
- "443:443" # HTTPS (Caddy proxy)
|
||||
- "443:443/udp" # HTTP/3 (Caddy proxy)
|
||||
- "8080:8080" # Management UI (CPM+)
|
||||
environment:
|
||||
- CPM_ENV=production
|
||||
- CPM_HTTP_PORT=8080
|
||||
- CPM_DB_PATH=/app/data/cpm.db
|
||||
- CPM_FRONTEND_DIR=/app/frontend/dist
|
||||
- CPM_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CPM_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
- CPM_CADDY_BINARY=caddy
|
||||
- CPM_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CPM_IMPORT_DIR=/app/data/imports
|
||||
volumes:
|
||||
- cpm_data_local:/app/data
|
||||
- caddy_data_local:/data
|
||||
- caddy_config_local:/config
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
cpm_data_local:
|
||||
driver: local
|
||||
caddy_data_local:
|
||||
driver: local
|
||||
caddy_config_local:
|
||||
driver: local
|
||||
@@ -1,42 +0,0 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/wikid82/caddyproxymanagerplus:latest
|
||||
container_name: caddyproxymanagerplus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80" # HTTP (Caddy proxy)
|
||||
- "443:443" # HTTPS (Caddy proxy)
|
||||
- "443:443/udp" # HTTP/3 (Caddy proxy)
|
||||
- "8080:8080" # Management UI (CPM+)
|
||||
environment:
|
||||
- CPM_ENV=production
|
||||
- CPM_HTTP_PORT=8080
|
||||
- CPM_DB_PATH=/app/data/cpm.db
|
||||
- CPM_FRONTEND_DIR=/app/frontend/dist
|
||||
- CPM_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CPM_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
- CPM_CADDY_BINARY=caddy
|
||||
- CPM_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CPM_IMPORT_DIR=/app/data/imports
|
||||
volumes:
|
||||
- cpm_data:/app/data
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
# Mount your existing Caddyfile for automatic import (optional)
|
||||
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
cpm_data:
|
||||
driver: local
|
||||
caddy_data:
|
||||
driver: local
|
||||
caddy_config:
|
||||
driver: local
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Entrypoint script to run both Caddy and CPM+ in a single container
|
||||
# This simplifies deployment for home users
|
||||
|
||||
echo "Starting CaddyProxyManager+ with integrated Caddy..."
|
||||
|
||||
# Start Caddy in the background with initial empty config
|
||||
echo '{"apps":{}}' > /config/caddy.json
|
||||
# Use JSON config directly; no adapter needed
|
||||
caddy run --config /config/caddy.json &
|
||||
CADDY_PID=$!
|
||||
echo "Caddy started (PID: $CADDY_PID)"
|
||||
|
||||
# Wait for Caddy to be ready
|
||||
echo "Waiting for Caddy admin API..."
|
||||
i=1
|
||||
while [ "$i" -le 30 ]; do
|
||||
if wget -q -O- http://127.0.0.1:2019/config/ > /dev/null 2>&1; then
|
||||
echo "Caddy is ready!"
|
||||
break
|
||||
fi
|
||||
i=$((i+1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Start CPM+ management application
|
||||
echo "Starting CPM+ management application..."
|
||||
/app/cpmp &
|
||||
APP_PID=$!
|
||||
echo "CPM+ started (PID: $APP_PID)"
|
||||
|
||||
# Function to handle shutdown gracefully
|
||||
shutdown() {
|
||||
echo "Shutting down..."
|
||||
kill -TERM "$APP_PID" 2>/dev/null || true
|
||||
kill -TERM "$CADDY_PID" 2>/dev/null || true
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
wait "$CADDY_PID" 2>/dev/null || true
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Trap signals for graceful shutdown
|
||||
trap 'shutdown' TERM INT
|
||||
|
||||
echo "CaddyProxyManager+ is running!"
|
||||
echo " - Management UI: http://localhost:8080"
|
||||
echo " - Caddy Proxy: http://localhost:80, https://localhost:443"
|
||||
echo " - Caddy Admin API: http://localhost:2019"
|
||||
|
||||
# Wait loop: exit when either process dies, then shutdown the other
|
||||
while kill -0 "$APP_PID" 2>/dev/null && kill -0 "$CADDY_PID" 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "A process exited, initiating shutdown..."
|
||||
shutdown
|
||||
-669
@@ -1,669 +0,0 @@
|
||||
# API Documentation
|
||||
|
||||
CaddyProxyManager+ REST API documentation. All endpoints return JSON and use standard HTTP status codes.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
http://localhost:8080/api/v1
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
🚧 Authentication is not yet implemented. All endpoints are currently public.
|
||||
|
||||
Future authentication will use JWT tokens:
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Example",
|
||||
"created_at": "2025-01-18T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Resource not found",
|
||||
"code": 404
|
||||
}
|
||||
```
|
||||
|
||||
## Status Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 200 | Success |
|
||||
| 201 | Created |
|
||||
| 204 | No Content (successful deletion) |
|
||||
| 400 | Bad Request (validation error) |
|
||||
| 404 | Not Found |
|
||||
| 500 | Internal Server Error |
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Health Check
|
||||
|
||||
Check API health status.
|
||||
|
||||
```http
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Proxy Hosts
|
||||
|
||||
#### List All Proxy Hosts
|
||||
|
||||
```http
|
||||
GET /proxy-hosts
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"domain": "example.com, www.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"ssl_forced": false,
|
||||
"http2_support": true,
|
||||
"hsts_enabled": false,
|
||||
"hsts_subdomains": false,
|
||||
"block_exploits": true,
|
||||
"websocket_support": false,
|
||||
"enabled": true,
|
||||
"remote_server_id": null,
|
||||
"created_at": "2025-01-18T10:00:00Z",
|
||||
"updated_at": "2025-01-18T10:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Get Proxy Host
|
||||
|
||||
```http
|
||||
GET /proxy-hosts/:uuid
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `uuid` (path) - Proxy host UUID
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"domain": "example.com",
|
||||
"forward_scheme": "https",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 9000,
|
||||
"ssl_forced": true,
|
||||
"websocket_support": false,
|
||||
"enabled": true,
|
||||
"created_at": "2025-01-18T10:00:00Z",
|
||||
"updated_at": "2025-01-18T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404:**
|
||||
```json
|
||||
{
|
||||
"error": "Proxy host not found"
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Proxy Host
|
||||
|
||||
```http
|
||||
POST /proxy-hosts
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"domain": "new.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 3000,
|
||||
"ssl_forced": false,
|
||||
"http2_support": true,
|
||||
"hsts_enabled": false,
|
||||
"hsts_subdomains": false,
|
||||
"block_exploits": true,
|
||||
"websocket_support": false,
|
||||
"enabled": true,
|
||||
"remote_server_id": null
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `domain` - Domain name(s), comma-separated
|
||||
- `forward_host` - Target hostname or IP
|
||||
- `forward_port` - Target port number
|
||||
|
||||
**Optional Fields:**
|
||||
- `forward_scheme` - Default: `"http"`
|
||||
- `ssl_forced` - Default: `false`
|
||||
- `http2_support` - Default: `true`
|
||||
- `hsts_enabled` - Default: `false`
|
||||
- `hsts_subdomains` - Default: `false`
|
||||
- `block_exploits` - Default: `true`
|
||||
- `websocket_support` - Default: `false`
|
||||
- `enabled` - Default: `true`
|
||||
- `remote_server_id` - Default: `null`
|
||||
|
||||
**Response 201:**
|
||||
```json
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"domain": "new.example.com",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 3000,
|
||||
"created_at": "2025-01-18T10:05:00Z",
|
||||
"updated_at": "2025-01-18T10:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400:**
|
||||
```json
|
||||
{
|
||||
"error": "domain is required"
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Proxy Host
|
||||
|
||||
```http
|
||||
PUT /proxy-hosts/:uuid
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `uuid` (path) - Proxy host UUID
|
||||
|
||||
**Request Body:** (all fields optional)
|
||||
```json
|
||||
{
|
||||
"domain": "updated.example.com",
|
||||
"forward_port": 8081,
|
||||
"ssl_forced": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"domain": "updated.example.com",
|
||||
"forward_port": 8081,
|
||||
"ssl_forced": true,
|
||||
"updated_at": "2025-01-18T10:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete Proxy Host
|
||||
|
||||
```http
|
||||
DELETE /proxy-hosts/:uuid
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `uuid` (path) - Proxy host UUID
|
||||
|
||||
**Response 204:** No content
|
||||
|
||||
**Response 404:**
|
||||
```json
|
||||
{
|
||||
"error": "Proxy host not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Remote Servers
|
||||
|
||||
#### List All Remote Servers
|
||||
|
||||
```http
|
||||
GET /remote-servers
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `enabled` (optional) - Filter by enabled status (`true` or `false`)
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"uuid": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Docker Registry",
|
||||
"provider": "docker",
|
||||
"host": "registry.local",
|
||||
"port": 5000,
|
||||
"reachable": true,
|
||||
"last_checked": "2025-01-18T09:55:00Z",
|
||||
"enabled": true,
|
||||
"created_at": "2025-01-18T09:00:00Z",
|
||||
"updated_at": "2025-01-18T09:55:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Get Remote Server
|
||||
|
||||
```http
|
||||
GET /remote-servers/:uuid
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `uuid` (path) - Remote server UUID
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"uuid": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Docker Registry",
|
||||
"provider": "docker",
|
||||
"host": "registry.local",
|
||||
"port": 5000,
|
||||
"reachable": true,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Remote Server
|
||||
|
||||
```http
|
||||
POST /remote-servers
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Production API",
|
||||
"provider": "generic",
|
||||
"host": "api.prod.internal",
|
||||
"port": 8080,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `name` - Server name
|
||||
- `host` - Hostname or IP
|
||||
- `port` - Port number
|
||||
|
||||
**Optional Fields:**
|
||||
- `provider` - One of: `generic`, `docker`, `kubernetes`, `aws`, `gcp`, `azure` (default: `generic`)
|
||||
- `enabled` - Default: `true`
|
||||
|
||||
**Response 201:**
|
||||
```json
|
||||
{
|
||||
"uuid": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"name": "Production API",
|
||||
"provider": "generic",
|
||||
"host": "api.prod.internal",
|
||||
"port": 8080,
|
||||
"reachable": false,
|
||||
"enabled": true,
|
||||
"created_at": "2025-01-18T10:15:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Remote Server
|
||||
|
||||
```http
|
||||
PUT /remote-servers/:uuid
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:** (all fields optional)
|
||||
```json
|
||||
{
|
||||
"name": "Updated Name",
|
||||
"port": 8081,
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"uuid": "660e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Updated Name",
|
||||
"port": 8081,
|
||||
"enabled": false,
|
||||
"updated_at": "2025-01-18T10:20:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete Remote Server
|
||||
|
||||
```http
|
||||
DELETE /remote-servers/:uuid
|
||||
```
|
||||
|
||||
**Response 204:** No content
|
||||
|
||||
#### Test Remote Server Connection
|
||||
|
||||
Test connectivity to a remote server.
|
||||
|
||||
```http
|
||||
POST /remote-servers/:uuid/test
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `uuid` (path) - Remote server UUID
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"reachable": true,
|
||||
"address": "registry.local:5000",
|
||||
"timestamp": "2025-01-18T10:25:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200 (unreachable):**
|
||||
```json
|
||||
{
|
||||
"reachable": false,
|
||||
"address": "offline.server:8080",
|
||||
"error": "connection timeout",
|
||||
"timestamp": "2025-01-18T10:25:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** This endpoint updates the `reachable` and `last_checked` fields on the remote server.
|
||||
|
||||
---
|
||||
|
||||
### Import Workflow
|
||||
|
||||
#### Check Import Status
|
||||
|
||||
Check if there's an active import session.
|
||||
|
||||
```http
|
||||
GET /import/status
|
||||
```
|
||||
|
||||
**Response 200 (no session):**
|
||||
```json
|
||||
{
|
||||
"has_pending": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200 (active session):**
|
||||
```json
|
||||
{
|
||||
"has_pending": true,
|
||||
"session": {
|
||||
"uuid": "770e8400-e29b-41d4-a716-446655440000",
|
||||
"filename": "Caddyfile",
|
||||
"state": "reviewing",
|
||||
"created_at": "2025-01-18T10:30:00Z",
|
||||
"updated_at": "2025-01-18T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Import Preview
|
||||
|
||||
Get preview of hosts to be imported (only available when session state is `reviewing`).
|
||||
|
||||
```http
|
||||
GET /import/preview
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"hosts": [
|
||||
{
|
||||
"domain": "example.com",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"forward_scheme": "http"
|
||||
},
|
||||
{
|
||||
"domain": "api.example.com",
|
||||
"forward_host": "backend",
|
||||
"forward_port": 9000,
|
||||
"forward_scheme": "https"
|
||||
}
|
||||
],
|
||||
"conflicts": [
|
||||
"example.com already exists"
|
||||
],
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404:**
|
||||
```json
|
||||
{
|
||||
"error": "No active import session"
|
||||
}
|
||||
```
|
||||
|
||||
#### Upload Caddyfile
|
||||
|
||||
Upload a Caddyfile for import.
|
||||
|
||||
```http
|
||||
POST /import/upload
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"content": "example.com {\n reverse_proxy localhost:8080\n}",
|
||||
"filename": "Caddyfile"
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `content` - Caddyfile content
|
||||
|
||||
**Optional Fields:**
|
||||
- `filename` - Original filename (default: `"Caddyfile"`)
|
||||
|
||||
**Response 201:**
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"uuid": "770e8400-e29b-41d4-a716-446655440000",
|
||||
"filename": "Caddyfile",
|
||||
"state": "parsing",
|
||||
"created_at": "2025-01-18T10:35:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400:**
|
||||
```json
|
||||
{
|
||||
"error": "content is required"
|
||||
}
|
||||
```
|
||||
|
||||
#### Commit Import
|
||||
|
||||
Commit the import after resolving conflicts.
|
||||
|
||||
```http
|
||||
POST /import/commit
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"session_uuid": "770e8400-e29b-41d4-a716-446655440000",
|
||||
"resolutions": {
|
||||
"example.com": "overwrite",
|
||||
"api.example.com": "keep"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `session_uuid` - Active import session UUID
|
||||
- `resolutions` - Map of domain to resolution strategy
|
||||
|
||||
**Resolution Strategies:**
|
||||
- `"keep"` - Keep existing configuration, skip import
|
||||
- `"overwrite"` - Replace existing with imported configuration
|
||||
- `"skip"` - Same as keep
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"imported": 2,
|
||||
"skipped": 1,
|
||||
"failed": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400:**
|
||||
```json
|
||||
{
|
||||
"error": "Invalid session or unresolved conflicts"
|
||||
}
|
||||
```
|
||||
|
||||
#### Cancel Import
|
||||
|
||||
Cancel an active import session.
|
||||
|
||||
```http
|
||||
DELETE /import/cancel?session_uuid=770e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `session_uuid` - Active import session UUID
|
||||
|
||||
**Response 204:** No content
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
🚧 Rate limiting is not yet implemented.
|
||||
|
||||
Future rate limits:
|
||||
- 100 requests per minute per IP
|
||||
- 1000 requests per hour per IP
|
||||
|
||||
## Pagination
|
||||
|
||||
🚧 Pagination is not yet implemented.
|
||||
|
||||
Future pagination:
|
||||
```http
|
||||
GET /proxy-hosts?page=1&per_page=20
|
||||
```
|
||||
|
||||
## Filtering and Sorting
|
||||
|
||||
🚧 Advanced filtering is not yet implemented.
|
||||
|
||||
Future filtering:
|
||||
```http
|
||||
GET /proxy-hosts?enabled=true&sort=created_at&order=desc
|
||||
```
|
||||
|
||||
## Webhooks
|
||||
|
||||
🚧 Webhooks are not yet implemented.
|
||||
|
||||
Future webhook events:
|
||||
- `proxy_host.created`
|
||||
- `proxy_host.updated`
|
||||
- `proxy_host.deleted`
|
||||
- `remote_server.unreachable`
|
||||
- `import.completed`
|
||||
|
||||
## SDKs
|
||||
|
||||
No official SDKs yet. The API follows REST conventions and can be used with any HTTP client.
|
||||
|
||||
### JavaScript/TypeScript Example
|
||||
|
||||
```typescript
|
||||
const API_BASE = 'http://localhost:8080/api/v1';
|
||||
|
||||
// List proxy hosts
|
||||
const hosts = await fetch(`${API_BASE}/proxy-hosts`).then(r => r.json());
|
||||
|
||||
// Create proxy host
|
||||
const newHost = await fetch(`${API_BASE}/proxy-hosts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
domain: 'example.com',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080
|
||||
})
|
||||
}).then(r => r.json());
|
||||
|
||||
// Test remote server
|
||||
const testResult = await fetch(`${API_BASE}/remote-servers/${uuid}/test`, {
|
||||
method: 'POST'
|
||||
}).then(r => r.json());
|
||||
```
|
||||
|
||||
### Python Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
API_BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
# List proxy hosts
|
||||
hosts = requests.get(f'{API_BASE}/proxy-hosts').json()
|
||||
|
||||
# Create proxy host
|
||||
new_host = requests.post(f'{API_BASE}/proxy-hosts', json={
|
||||
'domain': 'example.com',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080
|
||||
}).json()
|
||||
|
||||
# Test remote server
|
||||
test_result = requests.post(f'{API_BASE}/remote-servers/{uuid}/test').json()
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For API issues or questions:
|
||||
- GitHub Issues: https://github.com/Wikid82/CaddyProxyManagerPlus/issues
|
||||
- Discussions: https://github.com/Wikid82/CaddyProxyManagerPlus/discussions
|
||||
@@ -1,337 +0,0 @@
|
||||
# Database Schema Documentation
|
||||
|
||||
CaddyProxyManager+ uses SQLite with GORM ORM for data persistence. This document describes the database schema and relationships.
|
||||
|
||||
## Overview
|
||||
|
||||
The database consists of 8 main tables:
|
||||
- ProxyHost
|
||||
- RemoteServer
|
||||
- CaddyConfig
|
||||
- SSLCertificate
|
||||
- AccessList
|
||||
- User
|
||||
- Setting
|
||||
- ImportSession
|
||||
|
||||
## Entity Relationship Diagram
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ ProxyHost │
|
||||
├─────────────────┤
|
||||
│ UUID │◄──┐
|
||||
│ Domain │ │
|
||||
│ ForwardScheme │ │
|
||||
│ ForwardHost │ │
|
||||
│ ForwardPort │ │
|
||||
│ SSLForced │ │
|
||||
│ WebSocketSupport│ │
|
||||
│ Enabled │ │
|
||||
│ RemoteServerID │───┘ (optional)
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
│
|
||||
│ 1:1
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ CaddyConfig │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ ProxyHostID │
|
||||
│ RawConfig │
|
||||
│ GeneratedAt │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ RemoteServer │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ Name │
|
||||
│ Provider │
|
||||
│ Host │
|
||||
│ Port │
|
||||
│ Reachable │
|
||||
│ LastChecked │
|
||||
│ Enabled │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ SSLCertificate │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ Name │
|
||||
│ DomainNames │
|
||||
│ CertPEM │
|
||||
│ KeyPEM │
|
||||
│ ExpiresAt │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ AccessList │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ Name │
|
||||
│ Addresses │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ User │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ Email │
|
||||
│ PasswordHash │
|
||||
│ IsActive │
|
||||
│ IsAdmin │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ Setting │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ Key │ (unique)
|
||||
│ Value │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ ImportSession │
|
||||
├─────────────────┤
|
||||
│ UUID │
|
||||
│ Filename │
|
||||
│ State │
|
||||
│ CreatedAt │
|
||||
│ UpdatedAt │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Table Details
|
||||
|
||||
### ProxyHost
|
||||
|
||||
Stores reverse proxy host configurations.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `domain` | TEXT | Domain names (comma-separated) |
|
||||
| `forward_scheme` | TEXT | http or https |
|
||||
| `forward_host` | TEXT | Target server hostname/IP |
|
||||
| `forward_port` | INTEGER | Target server port |
|
||||
| `ssl_forced` | BOOLEAN | Force HTTPS redirect |
|
||||
| `http2_support` | BOOLEAN | Enable HTTP/2 |
|
||||
| `hsts_enabled` | BOOLEAN | Enable HSTS header |
|
||||
| `hsts_subdomains` | BOOLEAN | Include subdomains in HSTS |
|
||||
| `block_exploits` | BOOLEAN | Block common exploits |
|
||||
| `websocket_support` | BOOLEAN | Enable WebSocket proxying |
|
||||
| `enabled` | BOOLEAN | Proxy is active |
|
||||
| `remote_server_id` | UUID | Foreign key to RemoteServer (nullable) |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- Primary key on `uuid`
|
||||
- Foreign key index on `remote_server_id`
|
||||
|
||||
**Relationships:**
|
||||
- `RemoteServer`: Many-to-One (optional) - Links to remote Caddy instance
|
||||
- `CaddyConfig`: One-to-One - Generated Caddyfile configuration
|
||||
|
||||
### RemoteServer
|
||||
|
||||
Stores remote Caddy server connection information.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `name` | TEXT | Friendly name |
|
||||
| `provider` | TEXT | generic, docker, kubernetes, aws, gcp, azure |
|
||||
| `host` | TEXT | Hostname or IP address |
|
||||
| `port` | INTEGER | Port number (default 2019) |
|
||||
| `reachable` | BOOLEAN | Connection test result |
|
||||
| `last_checked` | TIMESTAMP | Last connection test time |
|
||||
| `enabled` | BOOLEAN | Server is active |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- Primary key on `uuid`
|
||||
- Index on `enabled` for fast filtering
|
||||
|
||||
### CaddyConfig
|
||||
|
||||
Stores generated Caddyfile configurations for each proxy host.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `proxy_host_id` | UUID | Foreign key to ProxyHost |
|
||||
| `raw_config` | TEXT | Generated Caddyfile content |
|
||||
| `generated_at` | TIMESTAMP | When config was generated |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- Primary key on `uuid`
|
||||
- Unique index on `proxy_host_id`
|
||||
|
||||
### SSLCertificate
|
||||
|
||||
Stores SSL/TLS certificates (future enhancement).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `name` | TEXT | Certificate name |
|
||||
| `domain_names` | TEXT | Domains covered (comma-separated) |
|
||||
| `cert_pem` | TEXT | Certificate in PEM format |
|
||||
| `key_pem` | TEXT | Private key in PEM format |
|
||||
| `expires_at` | TIMESTAMP | Certificate expiration |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
### AccessList
|
||||
|
||||
Stores IP-based access control lists (future enhancement).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `name` | TEXT | List name |
|
||||
| `addresses` | TEXT | IP addresses (comma-separated) |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
### User
|
||||
|
||||
Stores user authentication information (future enhancement).
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `email` | TEXT | Email address (unique) |
|
||||
| `password_hash` | TEXT | Bcrypt password hash |
|
||||
| `is_active` | BOOLEAN | Account is active |
|
||||
| `is_admin` | BOOLEAN | Admin privileges |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- Primary key on `uuid`
|
||||
- Unique index on `email`
|
||||
|
||||
### Setting
|
||||
|
||||
Stores application-wide settings as key-value pairs.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `key` | TEXT | Setting key (unique) |
|
||||
| `value` | TEXT | Setting value (JSON string) |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- Primary key on `uuid`
|
||||
- Unique index on `key`
|
||||
|
||||
**Default Settings:**
|
||||
- `app_name`: "CaddyProxyManager+"
|
||||
- `default_scheme`: "http"
|
||||
- `enable_ssl_by_default`: "false"
|
||||
|
||||
### ImportSession
|
||||
|
||||
Tracks Caddyfile import sessions.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `uuid` | UUID | Primary key |
|
||||
| `filename` | TEXT | Uploaded filename (optional) |
|
||||
| `state` | TEXT | parsing, reviewing, completed, failed |
|
||||
| `created_at` | TIMESTAMP | Creation timestamp |
|
||||
| `updated_at` | TIMESTAMP | Last update timestamp |
|
||||
|
||||
**States:**
|
||||
- `parsing`: Caddyfile is being parsed
|
||||
- `reviewing`: Waiting for user to review/resolve conflicts
|
||||
- `completed`: Import successfully committed
|
||||
- `failed`: Import failed with errors
|
||||
|
||||
## Database Initialization
|
||||
|
||||
The database is automatically created and migrated when the application starts. Use the seed script to populate with sample data:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go run ./cmd/seed/main.go
|
||||
```
|
||||
|
||||
### Sample Seed Data
|
||||
|
||||
The seed script creates:
|
||||
- 4 remote servers (Docker registry, API server, web app, database admin)
|
||||
- 3 proxy hosts (app.local.dev, api.local.dev, docker.local.dev)
|
||||
- 3 settings (app configuration)
|
||||
- 1 admin user
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
GORM AutoMigrate is used for schema migrations:
|
||||
|
||||
```go
|
||||
db.AutoMigrate(
|
||||
&models.ProxyHost{},
|
||||
&models.RemoteServer{},
|
||||
&models.CaddyConfig{},
|
||||
&models.SSLCertificate{},
|
||||
&models.AccessList{},
|
||||
&models.User{},
|
||||
&models.Setting{},
|
||||
&models.ImportSession{},
|
||||
)
|
||||
```
|
||||
|
||||
This ensures the database schema stays in sync with model definitions.
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
cp backend/data/cpm.db backend/data/cpm.db.backup
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
cp backend/data/cpm.db.backup backend/data/cpm.db
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Indexes**: All foreign keys and frequently queried columns are indexed
|
||||
- **Connection Pooling**: GORM manages connection pooling automatically
|
||||
- **SQLite Pragmas**: `PRAGMA journal_mode=WAL` for better concurrency
|
||||
- **Query Optimization**: Use `.Preload()` for eager loading relationships
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Multi-tenancy support with organization model
|
||||
- Audit log table for tracking changes
|
||||
- Certificate auto-renewal tracking
|
||||
- Integration with Let's Encrypt
|
||||
- Metrics and monitoring data storage
|
||||
@@ -1,234 +0,0 @@
|
||||
# 🏠 Getting Started with Caddy Proxy Manager Plus
|
||||
|
||||
**Welcome!** This guide will walk you through setting up your first proxy. Don't worry if you're new to this - we'll explain everything step by step!
|
||||
|
||||
---
|
||||
|
||||
## 🤔 What Is This App?
|
||||
|
||||
Think of this app as a **traffic controller** for your websites and apps.
|
||||
|
||||
**Here's a simple analogy:**
|
||||
Imagine you have several houses (websites/apps) on different streets (servers). Instead of giving people complicated directions to each house, you have one main address (your domain) where a helpful guide (the proxy) sends visitors to the right house automatically.
|
||||
|
||||
**What you can do:**
|
||||
- ✅ Make multiple websites accessible through one domain
|
||||
- ✅ Route traffic from example.com to different servers
|
||||
- ✅ Manage SSL certificates (the lock icon in browsers)
|
||||
- ✅ Control who can access what
|
||||
|
||||
---
|
||||
|
||||
## 📋 Before You Start
|
||||
|
||||
You'll need:
|
||||
1. **A computer** (Windows, Mac, or Linux)
|
||||
2. **Docker installed** (it's like a magic box that runs apps)
|
||||
- Don't have it? [Get Docker here](https://docs.docker.com/get-docker/)
|
||||
3. **5 minutes** of your time
|
||||
|
||||
That's it! No programming needed.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Get the App Running
|
||||
|
||||
### The Easy Way (Recommended)
|
||||
|
||||
Open your **terminal** (or Command Prompt on Windows) and paste this:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-p 8080:8080 \
|
||||
-v caddy_data:/app/data \
|
||||
--name caddy-proxy-manager \
|
||||
ghcr.io/wikid82/cpmp:latest
|
||||
```
|
||||
|
||||
**What does this do?** It downloads and starts the app. You don't need to understand the details - just copy and paste!
|
||||
|
||||
### Check If It's Working
|
||||
|
||||
1. Open your web browser
|
||||
2. Go to: `http://localhost:8080`
|
||||
3. You should see the app! 🎉
|
||||
|
||||
> **Didn't work?** Check if Docker is running. On Windows/Mac, look for the Docker icon in your taskbar.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Step 2: Create Your First Proxy Host
|
||||
|
||||
Let's set up your first proxy! We'll create a simple example.
|
||||
|
||||
### What's a Proxy Host?
|
||||
|
||||
A **Proxy Host** is like a forwarding address. When someone visits `mysite.com`, it secretly sends them to `192.168.1.100:3000` without them knowing.
|
||||
|
||||
### Let's Create One!
|
||||
|
||||
1. **Click "Proxy Hosts"** in the left sidebar
|
||||
2. **Click "+ Add Proxy Host"** button (top right)
|
||||
3. **Fill in the form:**
|
||||
|
||||
📝 **Domain Name:** (What people type in their browser)
|
||||
```
|
||||
myapp.local
|
||||
```
|
||||
> This is like your house's street address
|
||||
|
||||
📍 **Forward To:** (Where the traffic goes)
|
||||
```
|
||||
192.168.1.100
|
||||
```
|
||||
> This is where your actual app is running
|
||||
|
||||
🔢 **Port:** (Which door to use)
|
||||
```
|
||||
3000
|
||||
```
|
||||
> Apps listen on specific "doors" (ports) - 3000 is common for web apps
|
||||
|
||||
🌐 **Scheme:** (How to talk to it)
|
||||
```
|
||||
http
|
||||
```
|
||||
> Choose `http` for most apps, `https` if your app already has SSL
|
||||
|
||||
4. **Click "Save"**
|
||||
|
||||
**Congratulations!** 🎉 You just created your first proxy! Now when you visit `http://myapp.local`, it will show your app from `192.168.1.100:3000`.
|
||||
|
||||
---
|
||||
|
||||
## 🌍 Step 3: Set Up a Remote Server (Optional)
|
||||
|
||||
Sometimes your apps are on different computers (servers). Let's add one!
|
||||
|
||||
### What's a Remote Server?
|
||||
|
||||
Think of it as **telling the app about other computers** you have. Once added, you can easily send traffic to them.
|
||||
|
||||
### Adding a Remote Server
|
||||
|
||||
1. **Click "Remote Servers"** in the left sidebar
|
||||
2. **Click "+ Add Server"** button
|
||||
3. **Fill in the details:**
|
||||
|
||||
🏷️ **Name:** (A friendly name)
|
||||
```
|
||||
My Home Server
|
||||
```
|
||||
|
||||
🌐 **Hostname:** (The address of your server)
|
||||
```
|
||||
192.168.1.50
|
||||
```
|
||||
|
||||
📝 **Description:** (Optional - helps you remember)
|
||||
```
|
||||
The server in my office running Docker
|
||||
```
|
||||
|
||||
4. **Click "Test Connection"** - this checks if the app can reach your server
|
||||
5. **Click "Save"**
|
||||
|
||||
Now when creating proxy hosts, you can pick this server from a dropdown instead of typing the address every time!
|
||||
|
||||
---
|
||||
|
||||
## 📥 Step 4: Import Existing Caddy Files (If You Have Them)
|
||||
|
||||
Already using Caddy and have configuration files? You can bring them in!
|
||||
|
||||
### What's a Caddyfile?
|
||||
|
||||
It's a **text file that tells Caddy how to route traffic**. If you're not sure if you have one, you probably don't need this step.
|
||||
|
||||
### How to Import
|
||||
|
||||
1. **Click "Import Caddy Config"** in the left sidebar
|
||||
2. **Choose your method:**
|
||||
- **Drag & Drop:** Just drag your `Caddyfile` into the box
|
||||
- **Paste:** Copy the contents and paste them in the text area
|
||||
3. **Click "Parse Config"** - the app reads your file
|
||||
4. **Review the results:**
|
||||
- ✅ Green items = imported successfully
|
||||
- ⚠️ Yellow items = need your attention (conflicts)
|
||||
- ❌ Red items = couldn't import (will show why)
|
||||
5. **Resolve any conflicts** (the app will guide you)
|
||||
6. **Click "Import Selected"**
|
||||
|
||||
Done! Your existing setup is now in the app.
|
||||
|
||||
> **Need more help?** Check the detailed [Import Guide](import-guide.md)
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips for New Users
|
||||
|
||||
### 1. Start Small
|
||||
Don't try to import everything at once. Start with one proxy host, make sure it works, then add more.
|
||||
|
||||
### 2. Use Test Connection
|
||||
When adding remote servers, always click "Test Connection" to make sure the app can reach them.
|
||||
|
||||
### 3. Check Your Ports
|
||||
Make sure the ports you use aren't already taken by other apps. Common ports:
|
||||
- `80` - Web traffic (HTTP)
|
||||
- `443` - Secure web traffic (HTTPS)
|
||||
- `3000-3999` - Apps often use these
|
||||
- `8080-8090` - Alternative web ports
|
||||
|
||||
### 4. Local Testing First
|
||||
Test everything with local addresses (like `localhost` or `192.168.x.x`) before using real domain names.
|
||||
|
||||
### 5. Save Backups
|
||||
The app stores everything in a database. The Docker command above saves it in `caddy_data` - don't delete this!
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Something Not Working?
|
||||
|
||||
### App Won't Start
|
||||
- **Check if Docker is running** - look for the Docker icon
|
||||
- **Check if port 8080 is free** - another app might be using it
|
||||
- **Try:** `docker ps` to see if it's running
|
||||
|
||||
### Can't Access the Website
|
||||
- **Check your spelling** - domain names are picky
|
||||
- **Check the port** - make sure the app is actually running on that port
|
||||
- **Check the firewall** - might be blocking connections
|
||||
|
||||
### Import Failed
|
||||
- **Check your Caddyfile syntax** - paste it at [Caddy Validate](https://caddyserver.com/docs/caddyfile)
|
||||
- **Look at the error message** - it usually tells you what's wrong
|
||||
- **Start with a simple file** - test with just one site first
|
||||
|
||||
---
|
||||
|
||||
## 📚 What's Next?
|
||||
|
||||
You now know the basics! Here's what to explore:
|
||||
|
||||
- 🔐 **Add SSL Certificates** - get the green lock icon
|
||||
- 🚦 **Set Up Access Lists** - control who can visit your sites
|
||||
- ⚙️ **Configure Settings** - customize the app
|
||||
- 🔌 **Try the API** - control everything with code
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Still Need Help?
|
||||
|
||||
We're here for you!
|
||||
|
||||
- 💬 [Ask on GitHub Discussions](https://github.com/Wikid82/CaddyProxyManagerPlus/discussions)
|
||||
- 🐛 [Report a Bug](https://github.com/Wikid82/CaddyProxyManagerPlus/issues)
|
||||
- 📖 [Read the Full Documentation](index.md)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>You're doing great! 🌟</strong><br>
|
||||
<em>Remember: Everyone was a beginner once. Take your time and have fun!</em>
|
||||
</p>
|
||||
@@ -1,259 +0,0 @@
|
||||
# 🔧 GitHub Setup Guide
|
||||
|
||||
This guide will help you set up GitHub Actions for automatic Docker builds and documentation deployment.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Step 1: Docker Image Publishing (Automatic!)
|
||||
|
||||
The Docker build workflow uses GitHub Container Registry (GHCR) to store your images. **No setup required!** GitHub automatically provides authentication tokens for GHCR.
|
||||
|
||||
### How It Works:
|
||||
|
||||
GitHub Actions automatically uses the built-in `GITHUB_TOKEN` which has permission to:
|
||||
- ✅ Push images to `ghcr.io/wikid82/caddyproxymanagerplus`
|
||||
- ✅ Link images to your repository
|
||||
- ✅ Publish images for free (public repositories)
|
||||
|
||||
**Nothing to configure!** Just push code and images will be built automatically.
|
||||
|
||||
### Make Your Images Public (Optional):
|
||||
|
||||
By default, container images are private. To make them public:
|
||||
|
||||
1. **Go to your repository** → https://github.com/Wikid82/CaddyProxyManagerPlus
|
||||
2. **Look for "Packages"** on the right sidebar (after first build)
|
||||
3. **Click your package name**
|
||||
4. **Click "Package settings"** (right side)
|
||||
5. **Scroll down to "Danger Zone"**
|
||||
6. **Click "Change visibility"** → Select **"Public"**
|
||||
|
||||
**Why make it public?** Anyone can pull your Docker images without authentication!
|
||||
|
||||
---
|
||||
|
||||
## 📚 Step 2: Enable GitHub Pages (For Documentation)
|
||||
|
||||
Your documentation will be published to GitHub Pages (not the wiki). Pages is better for auto-deployment and looks more professional!
|
||||
|
||||
### Enable Pages:
|
||||
|
||||
1. **Go to your repository** → https://github.com/Wikid82/CaddyProxyManagerPlus
|
||||
2. **Click "Settings"** (top menu)
|
||||
3. **Click "Pages"** (left sidebar under "Code and automation")
|
||||
4. **Under "Build and deployment":**
|
||||
- **Source**: Select **"GitHub Actions"** (not "Deploy from a branch")
|
||||
5. That's it! No other settings needed.
|
||||
|
||||
Once enabled, your docs will be live at:
|
||||
```
|
||||
https://wikid82.github.io/CaddyProxyManagerPlus/
|
||||
```
|
||||
|
||||
**Note:** The first deployment takes 2-3 minutes. Check the Actions tab to see progress!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 How the Workflows Work
|
||||
|
||||
### Docker Build Workflow (`.github/workflows/docker-build.yml`)
|
||||
|
||||
**Triggers when:**
|
||||
- ✅ You push to `main` branch → Creates `latest` tag
|
||||
- ✅ You push to `development` branch → Creates `dev` tag
|
||||
- ✅ You create a version tag like `v1.0.0` → Creates version tags
|
||||
- ✅ You manually trigger it from GitHub UI
|
||||
|
||||
**What it does:**
|
||||
1. Builds the frontend
|
||||
2. Builds a Docker image for multiple platforms (AMD64, ARM64)
|
||||
3. Pushes to Docker Hub with appropriate tags
|
||||
4. Tests the image by starting it and checking the health endpoint
|
||||
5. Shows you a summary of what was built
|
||||
|
||||
**Tags created:**
|
||||
- `latest` - Always the newest stable version (from `main`)
|
||||
- `dev` - The development version (from `development`)
|
||||
- `1.0.0`, `1.0`, `1` - Version numbers (from git tags)
|
||||
- `sha-abc1234` - Specific commit versions
|
||||
|
||||
**Where images are stored:**
|
||||
- `ghcr.io/wikid82/caddyproxymanagerplus:latest`
|
||||
- `ghcr.io/wikid82/caddyproxymanagerplus:dev`
|
||||
- `ghcr.io/wikid82/caddyproxymanagerplus:1.0.0`
|
||||
|
||||
### Documentation Workflow (`.github/workflows/docs.yml`)
|
||||
|
||||
**Triggers when:**
|
||||
- ✅ You push changes to `docs/` folder
|
||||
- ✅ You update `README.md`
|
||||
- ✅ You manually trigger it from GitHub UI
|
||||
|
||||
**What it does:**
|
||||
1. Converts all markdown files to beautiful HTML pages
|
||||
2. Creates a nice homepage with navigation
|
||||
3. Adds dark theme styling (matches the app!)
|
||||
4. Publishes to GitHub Pages
|
||||
5. Shows you the published URL
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Testing Your Setup
|
||||
|
||||
### Test Docker Build:
|
||||
|
||||
1. Make a small change to any file
|
||||
2. Commit and push to `development`:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "test: trigger docker build"
|
||||
git push origin development
|
||||
```
|
||||
3. Go to **Actions** tab on GitHub
|
||||
4. Watch the "Build and Push Docker Images" workflow run
|
||||
5. Check **Packages** on your GitHub profile for the new `dev` tag!
|
||||
|
||||
### Test Docs Deployment:
|
||||
|
||||
1. Make a small change to `README.md` or any doc file
|
||||
2. Commit and push to `main`:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "docs: update readme"
|
||||
git push origin main
|
||||
```
|
||||
3. Go to **Actions** tab on GitHub
|
||||
4. Watch the "Deploy Documentation to GitHub Pages" workflow run
|
||||
5. Visit your docs site (shown in the workflow summary)!
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Creating Version Releases
|
||||
|
||||
When you're ready to release a new version:
|
||||
|
||||
1. **Tag your release:**
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "Release version 1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
2. **The workflow automatically:**
|
||||
- Builds Docker image
|
||||
- Tags it as `1.0.0`, `1.0`, and `1`
|
||||
- Pushes to Docker Hub
|
||||
- Tests it works
|
||||
|
||||
3. **Users can pull it:**
|
||||
```bash
|
||||
docker pull ghcr.io/wikid82/caddyproxymanagerplus:1.0.0
|
||||
docker pull ghcr.io/wikid82/caddyproxymanagerplus:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Docker Build Fails
|
||||
|
||||
**Problem**: "Error: denied: requested access to the resource is denied"
|
||||
- **Fix**: This shouldn't happen with `GITHUB_TOKEN` - check workflow permissions
|
||||
- **Verify**: Settings → Actions → General → Workflow permissions → "Read and write permissions" enabled
|
||||
|
||||
**Problem**: Can't pull the image
|
||||
- **Fix**: Make the package public (see Step 1 above)
|
||||
- **Or**: Authenticate with GitHub: `echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin`
|
||||
|
||||
### Docs Don't Deploy
|
||||
|
||||
**Problem**: "deployment not found"
|
||||
- **Fix**: Make sure you selected "GitHub Actions" as the source in Pages settings
|
||||
- **Not**: "Deploy from a branch"
|
||||
|
||||
**Problem**: Docs show 404 error
|
||||
- **Fix**: Wait 2-3 minutes after deployment completes
|
||||
- **Fix**: Check the workflow summary for the actual URL
|
||||
|
||||
### General Issues
|
||||
|
||||
**Check workflow logs:**
|
||||
1. Go to **Actions** tab
|
||||
2. Click the failed workflow
|
||||
3. Click the failed job
|
||||
4. Expand the step that failed
|
||||
5. Read the error message
|
||||
|
||||
**Still stuck?**
|
||||
- Open an issue: https://github.com/Wikid82/CaddyProxyManagerPlus/issues
|
||||
- We're here to help!
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Reference
|
||||
|
||||
### Docker Commands
|
||||
```bash
|
||||
# Pull latest development version
|
||||
docker pull ghcr.io/wikid82/caddyproxymanagerplus:dev
|
||||
|
||||
# Pull stable version
|
||||
docker pull ghcr.io/wikid82/cpmp:latest
|
||||
|
||||
# Pull specific version
|
||||
docker pull ghcr.io/wikid82/cpmp:1.0.0
|
||||
|
||||
# Run the container
|
||||
docker run -d -p 8080:8080 -v caddy_data:/app/data ghcr.io/wikid82/cpmp:latest
|
||||
```
|
||||
|
||||
### Git Tag Commands
|
||||
```bash
|
||||
# Create a new version tag
|
||||
git tag -a v1.2.3 -m "Release 1.2.3"
|
||||
|
||||
# Push the tag
|
||||
git push origin v1.2.3
|
||||
|
||||
# List all tags
|
||||
git tag -l
|
||||
|
||||
# Delete a tag (if you made a mistake)
|
||||
git tag -d v1.2.3
|
||||
git push origin :refs/tags/v1.2.3
|
||||
```
|
||||
|
||||
### Trigger Manual Workflow
|
||||
1. Go to **Actions** tab
|
||||
2. Click the workflow name (left sidebar)
|
||||
3. Click "Run workflow" button (right side)
|
||||
4. Select branch
|
||||
5. Click "Run workflow"
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
Before pushing to production, make sure:
|
||||
|
||||
- [ ] GitHub Pages is enabled with "GitHub Actions" source
|
||||
- [ ] You've tested the Docker build workflow (automatic on push)
|
||||
- [ ] You've tested the docs deployment workflow
|
||||
- [ ] Container package is set to "Public" visibility (optional, for easier pulls)
|
||||
- [ ] Documentation looks good on the published site
|
||||
- [ ] Docker image runs correctly
|
||||
- [ ] You've created your first version tag
|
||||
|
||||
---
|
||||
|
||||
## 🎉 You're Done!
|
||||
|
||||
Your CI/CD pipeline is now fully automated! Every time you:
|
||||
- Push to `main` → New `latest` Docker image + updated docs
|
||||
- Push to `development` → New `dev` Docker image for testing
|
||||
- Create a tag → New versioned Docker image
|
||||
|
||||
**No manual building needed!** 🚀
|
||||
|
||||
<p align="center">
|
||||
<em>Questions? Check the <a href="https://docs.github.com/en/actions">GitHub Actions docs</a> or <a href="https://github.com/Wikid82/CaddyProxyManagerPlus/issues">open an issue</a>!</em>
|
||||
</p>
|
||||
@@ -1,429 +0,0 @@
|
||||
# Caddyfile Import Guide
|
||||
|
||||
This guide explains how to import existing Caddyfiles into CaddyProxyManager+, handle conflicts, and troubleshoot common issues.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Import Methods](#import-methods)
|
||||
- [Import Workflow](#import-workflow)
|
||||
- [Conflict Resolution](#conflict-resolution)
|
||||
- [Supported Caddyfile Syntax](#supported-caddyfile-syntax)
|
||||
- [Limitations](#limitations)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Overview
|
||||
|
||||
CaddyProxyManager+ can import existing Caddyfiles and convert them into managed proxy host configurations. This is useful when:
|
||||
|
||||
- Migrating from standalone Caddy to CaddyProxyManager+
|
||||
- Importing configurations from other systems
|
||||
- Bulk importing multiple proxy hosts
|
||||
- Sharing configurations between environments
|
||||
|
||||
## Import Methods
|
||||
|
||||
### Method 1: File Upload
|
||||
|
||||
1. Navigate to **Import Caddyfile** page
|
||||
2. Click **Choose File** button
|
||||
3. Select your Caddyfile (any text file)
|
||||
4. Click **Upload**
|
||||
|
||||
### Method 2: Paste Content
|
||||
|
||||
1. Navigate to **Import Caddyfile** page
|
||||
2. Click **Paste Caddyfile** tab
|
||||
3. Paste your Caddyfile content into the textarea
|
||||
4. Click **Preview Import**
|
||||
|
||||
## Import Workflow
|
||||
|
||||
The import process follows these steps:
|
||||
|
||||
### 1. Upload/Paste
|
||||
|
||||
Upload your Caddyfile or paste the content directly.
|
||||
|
||||
```caddyfile
|
||||
# Example Caddyfile
|
||||
example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
|
||||
api.example.com {
|
||||
reverse_proxy https://backend:9000
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Parsing
|
||||
|
||||
The system parses your Caddyfile and extracts:
|
||||
- Domain names
|
||||
- Reverse proxy directives
|
||||
- TLS settings
|
||||
- Headers and other directives
|
||||
|
||||
**Parsing States:**
|
||||
- ✅ **Success** - All hosts parsed correctly
|
||||
- ⚠️ **Partial** - Some hosts parsed, others failed
|
||||
- ❌ **Failed** - Critical parsing error
|
||||
|
||||
### 3. Preview
|
||||
|
||||
Review the parsed configurations:
|
||||
|
||||
| Domain | Forward Host | Forward Port | SSL | Status |
|
||||
|--------|--------------|--------------|-----|--------|
|
||||
| example.com | localhost | 8080 | No | New |
|
||||
| api.example.com | backend | 9000 | Yes | New |
|
||||
|
||||
### 4. Conflict Detection
|
||||
|
||||
The system checks if any imported domains already exist:
|
||||
|
||||
- **No Conflicts** - All domains are new, safe to import
|
||||
- **Conflicts Found** - One or more domains already exist
|
||||
|
||||
### 5. Conflict Resolution
|
||||
|
||||
For each conflict, choose an action:
|
||||
|
||||
| Domain | Existing Config | New Config | Action |
|
||||
|--------|-----------------|------------|--------|
|
||||
| example.com | localhost:3000 | localhost:8080 | [Keep Existing ▼] |
|
||||
|
||||
**Resolution Options:**
|
||||
- **Keep Existing** - Don't import this host, keep current configuration
|
||||
- **Overwrite** - Replace existing configuration with imported one
|
||||
- **Skip** - Don't import this host, keep existing unchanged
|
||||
- **Create New** - Import as a new host with modified domain name
|
||||
|
||||
### 6. Commit
|
||||
|
||||
Once all conflicts are resolved, click **Commit Import** to finalize.
|
||||
|
||||
**Post-Import:**
|
||||
- Imported hosts appear in Proxy Hosts list
|
||||
- Configurations are saved to database
|
||||
- Caddy configs are generated automatically
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
### Strategy: Keep Existing
|
||||
|
||||
Use when you want to preserve your current configuration and ignore the imported one.
|
||||
|
||||
```
|
||||
Current: example.com → localhost:3000
|
||||
Imported: example.com → localhost:8080
|
||||
Result: example.com → localhost:3000 (unchanged)
|
||||
```
|
||||
|
||||
### Strategy: Overwrite
|
||||
|
||||
Use when the imported configuration is newer or more correct.
|
||||
|
||||
```
|
||||
Current: example.com → localhost:3000
|
||||
Imported: example.com → localhost:8080
|
||||
Result: example.com → localhost:8080 (replaced)
|
||||
```
|
||||
|
||||
### Strategy: Skip
|
||||
|
||||
Same as "Keep Existing" - imports everything except conflicting hosts.
|
||||
|
||||
### Strategy: Create New (Future)
|
||||
|
||||
Renames the imported host to avoid conflicts (e.g., `example.com` → `example-2.com`).
|
||||
|
||||
## Supported Caddyfile Syntax
|
||||
|
||||
### Basic Reverse Proxy
|
||||
|
||||
```caddyfile
|
||||
example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
**Parsed as:**
|
||||
- Domain: `example.com`
|
||||
- Forward Host: `localhost`
|
||||
- Forward Port: `8080`
|
||||
- Forward Scheme: `http`
|
||||
|
||||
### HTTPS Upstream
|
||||
|
||||
```caddyfile
|
||||
secure.example.com {
|
||||
reverse_proxy https://backend:9000
|
||||
}
|
||||
```
|
||||
|
||||
**Parsed as:**
|
||||
- Domain: `secure.example.com`
|
||||
- Forward Host: `backend`
|
||||
- Forward Port: `9000`
|
||||
- Forward Scheme: `https`
|
||||
|
||||
### Multiple Domains
|
||||
|
||||
```caddyfile
|
||||
example.com, www.example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
**Parsed as:**
|
||||
- Domain: `example.com, www.example.com`
|
||||
- Forward Host: `localhost`
|
||||
- Forward Port: `8080`
|
||||
|
||||
### TLS Configuration
|
||||
|
||||
```caddyfile
|
||||
example.com {
|
||||
tls internal
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
**Parsed as:**
|
||||
- SSL Forced: `true`
|
||||
- TLS provider: `internal` (self-signed)
|
||||
|
||||
### Headers and Directives
|
||||
|
||||
```caddyfile
|
||||
example.com {
|
||||
header {
|
||||
X-Custom-Header "value"
|
||||
}
|
||||
reverse_proxy localhost:8080 {
|
||||
header_up Host {host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Custom headers and advanced directives are stored in the raw CaddyConfig but may not be editable in the UI initially.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Current Limitations
|
||||
|
||||
1. **Path-based routing** - Not yet supported
|
||||
```caddyfile
|
||||
example.com {
|
||||
route /api/* {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
route /static/* {
|
||||
file_server
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **File server blocks** - Only reverse_proxy supported
|
||||
```caddyfile
|
||||
static.example.com {
|
||||
file_server
|
||||
root * /var/www/html
|
||||
}
|
||||
```
|
||||
|
||||
3. **Advanced matchers** - Basic domain matching only
|
||||
```caddyfile
|
||||
@api {
|
||||
path /api/*
|
||||
header X-API-Key *
|
||||
}
|
||||
reverse_proxy @api localhost:8080
|
||||
```
|
||||
|
||||
4. **Import statements** - Must be resolved before import
|
||||
```caddyfile
|
||||
import snippets/common.caddy
|
||||
```
|
||||
|
||||
5. **Environment variables** - Must be hardcoded
|
||||
```caddyfile
|
||||
{$DOMAIN} {
|
||||
reverse_proxy {$BACKEND_HOST}
|
||||
}
|
||||
```
|
||||
|
||||
### Workarounds
|
||||
|
||||
- **Path routing**: Create multiple proxy hosts per path
|
||||
- **File server**: Use separate Caddy instance or static host tool
|
||||
- **Matchers**: Manually configure in Caddy after import
|
||||
- **Imports**: Flatten your Caddyfile before importing
|
||||
- **Variables**: Replace with actual values before import
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Failed to parse Caddyfile"
|
||||
|
||||
**Cause:** Invalid Caddyfile syntax
|
||||
|
||||
**Solution:**
|
||||
1. Validate your Caddyfile with `caddy validate --config Caddyfile`
|
||||
2. Check for missing braces `{}`
|
||||
3. Ensure reverse_proxy directives are properly formatted
|
||||
|
||||
### Error: "No hosts found in Caddyfile"
|
||||
|
||||
**Cause:** Only contains directives without reverse_proxy blocks
|
||||
|
||||
**Solution:**
|
||||
- Ensure you have at least one `reverse_proxy` directive
|
||||
- Remove file_server-only blocks
|
||||
- Add domain blocks with reverse_proxy
|
||||
|
||||
### Warning: "Some hosts could not be imported"
|
||||
|
||||
**Cause:** Partial import with unsupported features
|
||||
|
||||
**Solution:**
|
||||
- Review the preview to see which hosts failed
|
||||
- Simplify complex directives
|
||||
- Import compatible hosts, add others manually
|
||||
|
||||
### Conflict Resolution Stuck
|
||||
|
||||
**Cause:** Not all conflicts have resolution selected
|
||||
|
||||
**Solution:**
|
||||
- Ensure every conflicting host has a resolution dropdown selection
|
||||
- The "Commit Import" button enables only when all conflicts are resolved
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Migration
|
||||
|
||||
**Original Caddyfile:**
|
||||
```caddyfile
|
||||
app.example.com {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
api.example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
**Import Result:**
|
||||
- 2 hosts imported successfully
|
||||
- No conflicts
|
||||
- Ready to use immediately
|
||||
|
||||
### Example 2: HTTPS Upstream
|
||||
|
||||
**Original Caddyfile:**
|
||||
```caddyfile
|
||||
secure.example.com {
|
||||
reverse_proxy https://internal.corp:9000 {
|
||||
transport http {
|
||||
tls_insecure_skip_verify
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Import Result:**
|
||||
- Domain: `secure.example.com`
|
||||
- Forward: `https://internal.corp:9000`
|
||||
- Note: `tls_insecure_skip_verify` stored in raw config
|
||||
|
||||
### Example 3: Multi-domain with Conflict
|
||||
|
||||
**Original Caddyfile:**
|
||||
```caddyfile
|
||||
example.com, www.example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
**Existing Configuration:**
|
||||
- `example.com` already points to `localhost:3000`
|
||||
|
||||
**Resolution:**
|
||||
1. System detects conflict on `example.com`
|
||||
2. Choose **Overwrite** to use new config
|
||||
3. Commit import
|
||||
4. Result: `example.com, www.example.com → localhost:8080`
|
||||
|
||||
### Example 4: Complex Setup (Partial Import)
|
||||
|
||||
**Original Caddyfile:**
|
||||
```caddyfile
|
||||
# Supported
|
||||
app.example.com {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
# Supported
|
||||
api.example.com {
|
||||
reverse_proxy https://backend:8080
|
||||
}
|
||||
|
||||
# NOT supported (file server)
|
||||
static.example.com {
|
||||
file_server
|
||||
root * /var/www
|
||||
}
|
||||
|
||||
# NOT supported (path routing)
|
||||
multi.example.com {
|
||||
route /api/* {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
route /web/* {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Import Result:**
|
||||
- ✅ `app.example.com` imported
|
||||
- ✅ `api.example.com` imported
|
||||
- ❌ `static.example.com` skipped (file_server not supported)
|
||||
- ❌ `multi.example.com` skipped (path routing not supported)
|
||||
- **Action:** Add unsupported hosts manually through UI or keep separate Caddyfile
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Validate First** - Run `caddy validate` before importing
|
||||
2. **Backup** - Keep a backup of your original Caddyfile
|
||||
3. **Simplify** - Remove unsupported directives before import
|
||||
4. **Test Small** - Import a few hosts first to verify
|
||||
5. **Review Preview** - Always check the preview before committing
|
||||
6. **Resolve Conflicts Carefully** - Understand impact before overwriting
|
||||
7. **Document Custom Config** - Note any advanced directives that can't be edited in UI
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check this guide's [Troubleshooting](#troubleshooting) section
|
||||
2. Review [Supported Syntax](#supported-caddyfile-syntax)
|
||||
3. Open an issue on GitHub with:
|
||||
- Your Caddyfile (sanitized)
|
||||
- Error messages
|
||||
- Expected vs actual behavior
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements to import functionality:
|
||||
|
||||
- [ ] Path-based routing support
|
||||
- [ ] Custom header import/export
|
||||
- [ ] Environment variable resolution
|
||||
- [ ] Import from URL
|
||||
- [ ] Export to Caddyfile
|
||||
- [ ] Diff view for conflicts
|
||||
- [ ] Batch import from multiple files
|
||||
- [ ] Import validation before upload
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
# 📚 Caddy Proxy Manager Plus - Documentation
|
||||
|
||||
Welcome! 👋 This page will help you find exactly what you need to use Caddy Proxy Manager Plus.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 I'm New Here - Where Do I Start?
|
||||
|
||||
Start with the [**README**](../README.md) - it's like the front door of our project! It will show you:
|
||||
- What this app does (in simple terms!)
|
||||
- How to install it on your computer
|
||||
- How to get it running in 5 minutes
|
||||
|
||||
**Next Step:** Once you have it running, check out the guides below!
|
||||
|
||||
---
|
||||
|
||||
## 📖 How-To Guides
|
||||
|
||||
### For Everyone
|
||||
|
||||
#### [🏠 Getting Started Guide](getting-started.md)
|
||||
*Coming soon!* - A step-by-step walkthrough of your first proxy setup. We'll hold your hand through the whole process!
|
||||
|
||||
#### [📥 Import Your Caddy Files](import-guide.md)
|
||||
Already have Caddy configuration files? This guide shows you how to bring them into the app so you don't have to start from scratch.
|
||||
|
||||
**What you'll learn:**
|
||||
- How to upload your existing files (it's just drag-and-drop!)
|
||||
- What to do if the app finds conflicts
|
||||
- Tips to make importing super smooth
|
||||
|
||||
---
|
||||
|
||||
### For Developers & Advanced Users
|
||||
|
||||
#### [🔌 API Documentation](api.md)
|
||||
Want to talk to the app using code? This guide shows all the ways you can send and receive information from the app.
|
||||
|
||||
**What you'll learn:**
|
||||
- All the different commands you can send
|
||||
- Examples in JavaScript and Python
|
||||
- What responses to expect
|
||||
|
||||
#### [💾 Database Guide](database-schema.md)
|
||||
Curious about how the app stores your information? This guide explains the database structure.
|
||||
|
||||
**What you'll learn:**
|
||||
- What information we save
|
||||
- How everything connects together
|
||||
- Tips for backing up your data
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Want to Help Make This Better?
|
||||
|
||||
#### [✨ Contributing Guide](../CONTRIBUTING.md)
|
||||
We'd love your help! This guide shows you how to:
|
||||
- Report bugs (things that don't work right)
|
||||
- Suggest new features
|
||||
- Submit code improvements
|
||||
- Follow our project rules
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Need Help?
|
||||
|
||||
### Quick Troubleshooting
|
||||
|
||||
**Can't get it to run?**
|
||||
- Check the [Installation section in README](../README.md#-installation)
|
||||
- Make sure Docker is installed and running
|
||||
- Try the quick start commands exactly as written
|
||||
|
||||
**Having import problems?**
|
||||
- See the [Import Guide troubleshooting section](import-guide.md#troubleshooting)
|
||||
- Check your Caddy file is valid
|
||||
- Look at the example files in the guide
|
||||
|
||||
**Found a bug?**
|
||||
- [Open an issue on GitHub](https://github.com/Wikid82/CaddyProxyManagerPlus/issues)
|
||||
- Tell us what you were trying to do
|
||||
- Share any error messages you see
|
||||
|
||||
---
|
||||
|
||||
## 📚 All Documentation Files
|
||||
|
||||
### User Documentation
|
||||
- [📖 README](../README.md) - Start here!
|
||||
- [📥 Import Guide](import-guide.md) - Bring in existing configs
|
||||
- [🏠 Getting Started](getting-started.md) - *Coming soon!*
|
||||
|
||||
### Developer Documentation
|
||||
- [🔌 API Reference](api.md) - REST API endpoints
|
||||
- [💾 Database Schema](database-schema.md) - How data is stored
|
||||
- [✨ Contributing](../CONTRIBUTING.md) - Help make this better
|
||||
- [🔧 GitHub Setup](github-setup.md) - Set up Docker builds & docs deployment
|
||||
|
||||
### Project Information
|
||||
- [📄 LICENSE](../LICENSE) - Legal stuff (MIT License)
|
||||
- [🔖 Changelog](../CHANGELOG.md) - *Coming soon!* - What's new in each version
|
||||
|
||||
---
|
||||
|
||||
## 💡 Quick Links
|
||||
|
||||
- [🏠 Project Home](https://github.com/Wikid82/CaddyProxyManagerPlus)
|
||||
- [🐛 Report a Bug](https://github.com/Wikid82/CaddyProxyManagerPlus/issues/new)
|
||||
- [💬 Ask a Question](https://github.com/Wikid82/CaddyProxyManagerPlus/discussions)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Made with ❤️ for the community</strong><br>
|
||||
<em>Questions? Open an issue - we're here to help!</em>
|
||||
</p>
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user