cloudpanel-api/
├── .github/ # GitHub specific files
│ └── workflows/ # GitHub Actions workflows
│ └── ci.yml
│
├── src/ # Source code
│ ├── config/ # Configuration files
│ │ ├── config.js # Main configuration
│ │ └── database.js # Database configuration
│ │
│ ├── controllers/ # Route controllers
│ │ ├── siteController.js
│ │ ├── databaseController.js
│ │ ├── userController.js
│ │ ├── certificateController.js
│ │ └── monitoringController.js
│ │
│ ├── middleware/ # Express middleware
│ │ ├── auth.js # API key authentication
│ │ ├── validation.js # Request validation
│ │ ├── security.js # Security middleware
│ │ ├── logging.js # Logging middleware
│ │ └── errorHandler.js # Error handling
│ │
│ ├── models/ # Database models
│ │ ├── Site.js
│ │ ├── Database.js
│ │ ├── User.js
│ │ ├── Certificate.js
│ │ └── ApiKey.js
│ │
│ ├── routes/ # API routes
│ │ ├── v1/ # API version 1
│ │ │ ├── sites.js
│ │ │ ├── databases.js
│ │ │ ├── users.js
│ │ │ ├── certificates.js
│ │ │ └── monitoring.js
│ │ └── index.js # Route aggregator
│ │
│ ├── services/ # Business logic
│ │ ├── siteService.js
│ │ ├── databaseService.js
│ │ ├── userService.js
│ │ └── monitoringService.js
│ │
│ ├── utils/ # Utility functions
│ │ ├── logger.js # Logging utility
│ │ ├── validation.js # Input validation
│ │ └── helpers.js # Helper functions
│ │
│ ├── metrics/ # Monitoring metrics
│ │ ├── prometheus.js
│ │ └── collectors.js
│ │
│ └── app.js # Express app setup
│
├── docker/ # Docker related files
│ ├── Dockerfile
│ └── docker-compose.yml
│
├── config/ # Configuration files
│ ├── prometheus/
│ │ └── prometheus.yml
│ ├── grafana/
│ │ └── datasources.yml
│ └── loki/
│ └── loki-config.yml
│
├── cloudpanel-scripts/ # Utility scripts
│
├── tests/ # Test files
│ ├── unit/
│ │ ├── controllers/
│ │ ├── services/
│ │ └── models/
│ ├── integration/
│ └── setup.js
│
├── docs/ # Documentation
│ ├── api/
│ │ └── swagger.yaml
│ ├── setup.md
│ └── monitoring.md
│
├── .env.example # Example environment variables
├── .eslintrc.js # ESLint configuration
├── .prettierrc # Prettier configuration
├── .gitignore
├── package.json
└── README.md
Contains all source code for the API. Organized by feature and responsibility.
Handle HTTP requests and responses. They use services for business logic.
Contains business logic and database interactions.
Express middleware for authentication, logging, etc.
Database models and schema definitions.
API route definitions, versioned in /v1
directory.
External service configurations (Prometheus, Grafana, etc.).
Docker-related files for containerization.
Test files organized by type (unit, integration).
API documentation and setup guides.
All API requests require an API key passed in the header:
X-API-Key: cp_your_api_key_here
Lists all sites.
Response:
{
"sites": [
{
"id": 1,
"domainName": "example.com",
"type": "php",
"rootDirectory": "/home/user/htdocs/example.com"
}
]
}
Create a new site.
Request:
{
"domainName": "newsite.com",
"type": "php",
"rootDirectory": "/home/user/htdocs/newsite.com",
"phpVersion": "8.2"
}
Lists all databases.
Create a new database.
Request:
{
"name": "mydb",
"siteId": 1,
"user": {
"username": "dbuser",
"password": "securepass"
}
}
Lists all users (requires admin API key).
Create a new user (requires admin API key).
Request:
{
"username": "newuser",
"email": "user@example.com",
"role": "user"
}
- 100 requests per 15 minutes per IP address
- Status 429 returned when exceeded
{
"error": "Error message here",
"code": "ERROR_CODE",
"details": {} // Optional additional information
}
When someone makes a request to create a site through the API endpoint (e.g., POST /api/v1/sites), here's what happens:
First, the API route handler in /src/routes/v1/sites.js
receives the request:
router.post('/', async (req, res) => {
try {
// Create an operation record in the database
const operationId = await db.run(`
INSERT INTO operations (
type, data, status, source, created_at
) VALUES (
'site.create',
?,
'pending',
'api',
datetime('now')
)
`, [JSON.stringify(req.body)]);
// Return immediate response with operation ID
res.status(202).json({
success: true,
operation_id: operationId,
message: 'Operation queued'
});
} catch (error) {
// Error handling...
}
});
Meanwhile, i have two systemd services running continuously:
- The queue worker (
queue_worker.sh
), which checks for new operations:
while true; do
# Query for pending operations
pending_ops=$(sqlite3 /home/clp/htdocs/app/data/db.sq3 "
SELECT id, type
FROM operations
WHERE status = 'pending'
AND source = 'api'
ORDER BY created_at ASC
")
if [[ -n "$pending_ops" ]]; then
# Process each operation...
fi
sleep 5
done
- The status monitor (
status_monitor.sh
), which watches for problems:
while true; do
# Check for stuck or timed out operations
check_stuck_operations
check_timed_out_operations
sleep 60
done
So when a site creation request comes in:
- API creates 'pending' operation in database
- Queue worker sees new operation within 5 seconds
- Queue worker runs appropriate script (e.g.,
manage_site.sh create $operation_id
) - Script processes operation and updates status to 'completed' or 'failed'
- Status monitor ensures nothing gets stuck
The client can poll the operation status endpoint to track progress:
router.get('/operations/:id', async (req, res) => {
const operation = await db.get(`
SELECT status, error, result
FROM operations
WHERE id = ?
`, [req.params.id]);
res.json({
success: true,
data: operation
});
});
This architecture gives us:
- Immediate API responses (non-blocking)
- Reliable operation processing
- Status tracking
- Error handling
- Separation from UI operations
Still working on UI API Script triger challange. The key is to create a distinction layer betien UI and API operations in CloudPanel.
The first step is to modify our core operation tracking in the database. When an operation comes in via the API, it should be tagged as an API operation. I can do this by adding a source
column to our operations table:
ALTER TABLE operations ADD COLUMN source VARCHAR(10) DEFAULT 'ui';
Then, I need to modify how our scripts check whether they should execute. the core database.sh script to include this check:
# Function to check if operation should be handled by scripts
should_handle_operation() {
local operation_id=$1
local source=$(sqlite3 /home/clp/htdocs/app/data/db.sq3 "
SELECT source
FROM operations
WHERE id = $operation_id
")
# Only handle operations that came from the API
[[ "$source" == "api" ]]
}
Now I can modify each of my handler scripts to use this check. For example, in manage_site.sh:
# Main execution function
main() {
OPERATION_ID=$2
local operation=$1
# First check if i should handle this operation
if ! should_handle_operation $OPERATION_ID; then
log_message "Operation $OPERATION_ID is not an API operation - skipping"
exit 0
fi
# Rest of the script continues as before...
For the API side, i need to ensure operations are properly tagged. When creating an operation through the API, i'll set the source:
create_operation() {
local type=$1
local data=$2
sqlite3 /home/clp/htdocs/app/data/db.sq3 "
INSERT INTO operations (
type, data, status, source, created_at
) VALUES (
'$type',
'$data',
'pending',
'api',
datetime('now')
)
"
}
This approach allows UI operations to continue using CloudPanel's built-in functionality while API operations go through our script system. This separation solves my chalanges:
- No interference with existing UI operations
- Clear tracking of operation sources
- Easy to maintain and debug
- No risk of duplicate operations
The queue worker and status monitor will naturally only process API operations since they'll inherit this check through the core database functions.