mock user accepted; must be in ADMIN_EMAILS * SSO enabled -> Authorization: Bearer required; validated via JWTValidator */ error_reporting(E_ALL); ini_set('display_errors', 0); ini_set('log_errors', 1); header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } require_once __DIR__ . '/config.php'; require_once __DIR__ . '/runtime_config.php'; // --------------------------------------------------------------------------- // Auth helpers // --------------------------------------------------------------------------- function getRequestUser(): ?array { // Always try Bearer token first — admin endpoint needs real identity even when // SSO_ENABLED=false (backend SSO disabled only means regular APIs skip auth, // not that admin panel should skip identity checks). $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (empty($authHeader) && function_exists('apache_request_headers')) { $headers = apache_request_headers(); $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? ''; } if (preg_match('/^Bearer\s+(.+)$/i', trim($authHeader), $m)) { require_once __DIR__ . '/JWTValidator.php'; $validator = new JWTValidator(SSO_TENANT_ID, SSO_CLIENT_ID); $result = $validator->validateToken($m[1]); if ($result['valid']) { return $result['payload']; } } // Fall back to mock user only for local dev (SSO fully disabled, no token sent) if (!SSO_ENABLED) { return ['preferred_username' => 'dev@localhost', 'name' => 'Local Developer']; } return null; } function isAdmin(?array $user): bool { if (!$user) return false; $list = array_filter(array_map('trim', explode(',', ADMIN_EMAILS))); if (empty($list)) return false; $email = strtolower( $user['preferred_username'] ?? $user['upn'] ?? $user['email'] ?? '' ); foreach ($list as $allowed) { if (strtolower($allowed) === $email) return true; } return false; } function requireAdmin(): array { $user = getRequestUser(); if (!isAdmin($user)) { http_response_code(403); echo json_encode(['error' => 'Admin access required']); exit; } return $user; } // --------------------------------------------------------------------------- // Kling test helper // --------------------------------------------------------------------------- function testKlingCredentials(string $accessKey, string $secretKey): array { $header = rtrim(strtr(base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])), '+/', '-_'), '='); $now = time(); $payload = rtrim(strtr(base64_encode(json_encode([ 'iss' => $accessKey, 'exp' => $now + 1800, 'nbf' => $now - 5, ])), '+/', '-_'), '='); $sig = rtrim(strtr(base64_encode(hash_hmac('sha256', "$header.$payload", $secretKey, true)), '+/', '-_'), '='); $jwt = "$header.$payload.$sig"; $ch = curl_init('https://api-singapore.klingai.com/v1/videos/text2video?pageNum=1&pageSize=1'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $jwt, ], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200) { return ['ok' => true]; } $body = json_decode($response, true); $error = $body['message'] ?? ($body['error'] ?? "HTTP $httpCode"); return ['ok' => false, 'error' => $error]; } // --------------------------------------------------------------------------- // Audit log // --------------------------------------------------------------------------- function appendAuditLog(string $action, array $user): void { $dir = __DIR__ . '/uploads'; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $line = json_encode([ 'ts' => gmdate('Y-m-d\TH:i:s\Z'), 'user' => $user['preferred_username'] ?? $user['email'] ?? 'unknown', 'action' => $action, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', ]) . "\n"; @file_put_contents($dir . '/admin_audit.log', $line, FILE_APPEND | LOCK_EX); } // --------------------------------------------------------------------------- // Request handler // --------------------------------------------------------------------------- try { $action = $_GET['action'] ?? $_POST['action'] ?? null; if ($action === 'status') { requireAdmin(); echo json_encode(['kling' => getKlingStatus()]); } elseif ($action === 'test_kling') { $user = requireAdmin(); $body = json_decode(file_get_contents('php://input'), true) ?? []; $accessKey = trim($body['access_key'] ?? ''); $secretKey = trim($body['secret_key'] ?? ''); if ($accessKey === '' || $secretKey === '') { $creds = getKlingCredentials(); $accessKey = $creds['access_key']; $secretKey = $creds['secret_key']; } if ($accessKey === '' || $secretKey === '') { echo json_encode(['ok' => false, 'error' => 'No credentials to test']); } else { echo json_encode(testKlingCredentials($accessKey, $secretKey)); } } elseif ($action === 'update_kling') { $user = requireAdmin(); $body = json_decode(file_get_contents('php://input'), true) ?? []; $accessKey = trim($body['access_key'] ?? ''); $secretKey = trim($body['secret_key'] ?? ''); if (strlen($accessKey) < 8 || strlen($secretKey) < 8) { http_response_code(400); echo json_encode(['error' => 'Access key and secret key must each be at least 8 characters']); exit; } $updatedBy = $user['preferred_username'] ?? $user['email'] ?? 'unknown'; if (!setKlingCredentials($accessKey, $secretKey, $updatedBy)) { http_response_code(500); echo json_encode(['error' => 'Failed to write runtime config — check file permissions on backend/']); exit; } appendAuditLog('update_kling', $user); echo json_encode(['kling' => getKlingStatus()]); } else { http_response_code(400); echo json_encode(['error' => 'Unknown action']); } } catch (Throwable $e) { error_log('admin_api.php error: ' . $e->getMessage()); http_response_code(500); echo json_encode(['error' => 'Internal server error']); }