When backend SSO_ENABLED=false, regular API endpoints skip auth, but the admin panel still needs to identify the caller. Now Bearer token is always validated first; mock dev@localhost fallback only kicks in when no token is present AND SSO is disabled (local dev). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
202 lines
7 KiB
PHP
202 lines
7 KiB
PHP
<?php
|
|
/**
|
|
* Admin API — Kling credential rotation and status
|
|
*
|
|
* Actions (all require admin):
|
|
* GET ?action=status — masked credential info
|
|
* POST ?action=update_kling — rotate access_key + secret_key
|
|
* POST ?action=test_kling — verify credentials against Kling API
|
|
*
|
|
* Authentication:
|
|
* SSO disabled -> mock user accepted; must be in ADMIN_EMAILS
|
|
* SSO enabled -> Authorization: Bearer <idToken> 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']);
|
|
}
|