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:
Wikid82
2025-11-19 22:53:32 -05:00
parent 1bc6be10a1
commit 1e2d87755d
178 changed files with 0 additions and 27250 deletions
-33
View File
@@ -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"
-76
View File
@@ -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/
-14
View File
@@ -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']
-93
View File
@@ -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
-38
View File
@@ -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.
-20
View File
@@ -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
-38
View File
@@ -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`.
-70
View File
@@ -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
}
]
}
-32
View File
@@ -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
-74
View File
@@ -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(', ')}`);
}
-62
View File
@@ -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,
});
-44
View File
@@ -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 }}"
-78
View File
@@ -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);
}
}
}
-277
View File
@@ -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
-134
View File
@@ -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'
-353
View File
@@ -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
-106
View File
@@ -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 }}
-58
View File
@@ -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
-52
View File
@@ -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
-27
View File
@@ -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
View File
@@ -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
-27
View File
@@ -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
-4
View File
@@ -1,4 +0,0 @@
version: 1
exclude:
- frontend/dist/**
- frontend/node_modules/**
-1
View File
@@ -1 +0,0 @@
0.1.0-alpha
-49
View File
@@ -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
View File
@@ -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+! 🎉
-234
View File
@@ -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)
-364
View File
@@ -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
View File
@@ -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"]
-262
View File
@@ -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/`! 🚀
-230
View File
@@ -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.
-21
View File
@@ -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.
-98
View File
@@ -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
-282
View File
@@ -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
-358
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-403
View File
@@ -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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://go.dev/)
[![React Version](https://img.shields.io/badge/React-18.3-61DAFB?logo=react)](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
View File
@@ -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"
```
-161
View File
@@ -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
-5
View File
@@ -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
-19
View File
@@ -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 ./...
```
-49
View File
@@ -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)
}
}
-204
View File
@@ -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.")
}
-48
View File
@@ -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
View File
@@ -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,
},
})
}
-55
View File
@@ -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()
}
}
-77
View File
@@ -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)
}
-101
View File
@@ -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
}
-95
View File
@@ -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)
}
-129
View File
@@ -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
}
-115
View File
@@ -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")
}
-263
View File
@@ -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
}
-206
View File
@@ -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)
}
-122
View File
@@ -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"`
}
-146
View File
@@ -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
}
-125
View File
@@ -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")
}
-59
View File
@@ -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
}
-18
View File
@@ -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
}
-19
View File
@@ -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"`
}
-14
View File
@@ -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"`
}
-21
View File
@@ -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"`
}
-18
View File
@@ -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"`
}
-26
View File
@@ -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"`
}
-24
View File
@@ -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"`
}
-16
View File
@@ -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"`
}
-40
View File
@@ -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
}
-21
View File
@@ -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
}
-139
View File
@@ -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
}
-20
View File
@@ -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
}
-1655
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
{
"devDependencies": {
"@vitest/coverage-v8": "^4.0.10"
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
-1238
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -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
-40
View File
@@ -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
-42
View File
@@ -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
-58
View File
@@ -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
View File
@@ -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
-337
View File
@@ -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
-234
View File
@@ -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>
-259
View File
@@ -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>
-429
View File
@@ -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
View File
@@ -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