Files
claudetools/projects/msp-tools/quote-wizard/php-api/api/routes/admin.php
Mike Swanson 068888202c Quote wizard: fix API URL and suPHP auth header handling
- Change production API URL from /msp-api to /quote/api
- Switch admin auth to X-Api-Key header as primary (suPHP strips Authorization)
- Keep Bearer token as fallback for PHP-FPM environments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:08:32 -07:00

146 lines
3.8 KiB
PHP

<?php
/**
* Admin route handlers for quote management.
*
* All handlers require a valid API key in the Authorization header.
* Format: Authorization: Bearer {ADMIN_API_KEY}
*/
// Deny direct access
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
http_response_code(403);
exit('Direct access denied.');
}
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../db.php';
require_once __DIR__ . '/../services/quote_service.php';
require_once __DIR__ . '/../services/syncro_service.php';
/**
* Verify the admin API key from the Authorization header.
*
* Expects: Authorization: Bearer {api_key}
* Terminates with 401 if missing or invalid.
*/
function check_admin_auth(): void
{
// suPHP strips the Authorization header, so accept X-Api-Key as primary
$token = $_SERVER['HTTP_X_API_KEY'] ?? '';
// Fallback: try Authorization: Bearer {key} (works with PHP-FPM)
if (empty($token)) {
$header = $_SERVER['HTTP_AUTHORIZATION']
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
?? '';
if (!empty($header) && strpos($header, 'Bearer ') === 0) {
$token = substr($header, 7);
}
}
if (empty($token)) {
error_response('API key required. Send X-Api-Key header.', 401);
}
if (ADMIN_API_KEY === 'CHANGE_ME_PLACEHOLDER') {
app_log('WARNING', '[WARNING] Admin API key is not configured (still placeholder)');
error_response('Admin API key not configured on server', 500);
}
if (!hash_equals(ADMIN_API_KEY, $token)) {
app_log('WARNING', '[WARNING] Invalid admin API key attempt from ' . (get_client_ip() ?? 'unknown'));
error_response('Invalid API key', 401);
}
}
/**
* GET /admin/quotes
*
* List quotes with pagination and optional filters.
* Query params: skip, limit, status, search
*/
function handle_list_quotes(): void
{
check_admin_auth();
$db = get_db();
$skip = max(0, (int)($_GET['skip'] ?? 0));
$limit = min(1000, max(1, (int)($_GET['limit'] ?? 100)));
$status = $_GET['status'] ?? null;
$search = $_GET['search'] ?? null;
// Validate status if provided
if ($status !== null && $status !== '' && !in_array($status, VALID_STATUSES, true)) {
error_response("Invalid status filter: {$status}", 400);
}
$result = list_quotes($db, $skip, $limit, $status, $search);
json_response([
'total' => $result['total'],
'skip' => $skip,
'limit' => $limit,
'quotes' => $result['quotes'],
]);
}
/**
* GET /admin/quotes/stats
*
* Get dashboard statistics for quotes.
*/
function handle_get_stats(): void
{
check_admin_auth();
$db = get_db();
$stats = get_stats($db);
json_response($stats);
}
/**
* GET /admin/quotes/{id}
*
* Get a single quote by ID with items, activities, and notifications.
*/
function handle_admin_get_quote(string $quote_id): void
{
check_admin_auth();
$db = get_db();
$quote = get_quote_by_id($db, $quote_id);
$response = build_admin_quote_response($db, $quote);
json_response($response);
}
/**
* PUT /admin/quotes/{id}
*
* Update a quote's status and/or expiration (admin only).
*/
function handle_admin_update_quote(string $quote_id): void
{
check_admin_auth();
$data = get_json_body();
$db = get_db();
$quote = admin_update_quote($db, $quote_id, $data, 'admin');
$response = build_admin_quote_response($db, $quote);
json_response($response);
}
/**
* POST /admin/quotes/{id}/sync-syncro
*
* Trigger a SyncroRMM sync for a quote.
*/
function handle_sync_syncro(string $quote_id): void
{
check_admin_auth();
$db = get_db();
$quote = get_quote_by_id($db, $quote_id);
$result = sync_quote_to_syncro($db, $quote);
json_response($result);
}