173 lines
5.8 KiB
PHP
173 lines
5.8 KiB
PHP
<?php
|
|
/**
|
|
* AVATAR PICKER V2 - SAFE UPDATE ENDPOINT
|
|
*
|
|
* POST /api/agent-avatar-update.php
|
|
* Body JSON:
|
|
* - agent: string (nom agent, requis)
|
|
* - emoji: string (optionnel)
|
|
* - url: string (optionnel, pour dicebear)
|
|
* - color: string (optionnel, hex)
|
|
*
|
|
* Merge non-écrasant dans agent-avatars-v2.json
|
|
* Backup auto avant chaque write (rotation keeps 10 derniers)
|
|
* Zero suppression, zero fake data, zero hardcode (doctrine)
|
|
*/
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type');
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
|
|
|
$SSOT = '/var/www/html/api/agent-avatars-v2.json';
|
|
$BAKDIR = '/var/www/html/api/avatar-backups';
|
|
@mkdir($BAKDIR, 0755, true);
|
|
$LOG = '/var/log/weval/avatar-update.log';
|
|
|
|
function jerr($code, $msg) {
|
|
http_response_code($code);
|
|
echo json_encode(['ok' => false, 'error' => $msg]);
|
|
exit;
|
|
}
|
|
|
|
function logline($m) {
|
|
global $LOG;
|
|
@file_put_contents($LOG, date('c') . " $m\n", FILE_APPEND);
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|
// Health check
|
|
$count = 0;
|
|
if (file_exists($SSOT)) {
|
|
$d = json_decode(file_get_contents($SSOT), true);
|
|
if (is_array($d)) $count = count($d);
|
|
}
|
|
$baks = glob("$BAKDIR/agent-avatars-v2.json.*") ?: [];
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'ssot' => $SSOT,
|
|
'exists' => file_exists($SSOT),
|
|
'count' => $count,
|
|
'backups' => count($baks),
|
|
'last_backup' => !empty($baks) ? basename(end($baks)) : null
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
jerr(405, 'Method not allowed');
|
|
}
|
|
|
|
$raw = file_get_contents('php://input');
|
|
$input = json_decode($raw, true);
|
|
if (!is_array($input)) jerr(400, 'Invalid JSON body');
|
|
|
|
$agent = trim($input['agent'] ?? '');
|
|
if ($agent === '') jerr(400, 'Missing agent name');
|
|
if (strlen($agent) > 200) jerr(400, 'Agent name too long');
|
|
|
|
// Load SSOT
|
|
if (!file_exists($SSOT)) jerr(500, 'SSOT file missing');
|
|
$data = json_decode(file_get_contents($SSOT), true);
|
|
if (!is_array($data)) jerr(500, 'SSOT JSON invalid');
|
|
|
|
if (!isset($data[$agent])) jerr(404, "Agent '$agent' not in SSOT");
|
|
|
|
// Accepted update fields (whitelist - doctrine zero hardcode of random keys)
|
|
$ALLOWED = ['emoji', 'url', 'color', 'role'];
|
|
$updates = [];
|
|
foreach ($ALLOWED as $k) {
|
|
if (array_key_exists($k, $input)) {
|
|
$v = $input[$k];
|
|
if ($v === '' || $v === null) continue; // skip empty
|
|
if (!is_string($v) && !is_null($v)) continue;
|
|
// Length caps
|
|
if ($k === 'emoji' && mb_strlen($v) > 20) jerr(400, 'emoji too long');
|
|
if ($k === 'url' && strlen($v) > 500) jerr(400, 'url too long');
|
|
if ($k === 'color' && !preg_match('/^#[0-9a-fA-F]{3,8}$/', $v)) jerr(400, 'invalid color hex');
|
|
if ($k === 'role' && strlen($v) > 30) jerr(400, 'role too long');
|
|
if ($k === 'url' && !preg_match('#^https?://#', $v)) jerr(400, 'url must be http(s)');
|
|
$updates[$k] = $v;
|
|
}
|
|
}
|
|
|
|
if (empty($updates)) jerr(400, 'No valid fields to update');
|
|
|
|
// Backup BEFORE write (doctrine GOLD)
|
|
$ts = date('Ymd-His');
|
|
$bakFile = "$BAKDIR/agent-avatars-v2.json.bak-$ts";
|
|
if (!@copy($SSOT, $bakFile)) jerr(500, 'Backup failed');
|
|
|
|
// Rotate: keep last 10 backups
|
|
$baks = glob("$BAKDIR/agent-avatars-v2.json.bak-*") ?: [];
|
|
sort($baks);
|
|
while (count($baks) > 10) {
|
|
@unlink(array_shift($baks));
|
|
}
|
|
|
|
// Merge (doctrine zero écrasement : on préserve tous les champs existants)
|
|
$before = $data[$agent];
|
|
foreach ($updates as $k => $v) {
|
|
$data[$agent][$k] = $v;
|
|
}
|
|
$after = $data[$agent];
|
|
|
|
// Write atomic via tmp
|
|
$tmp = $SSOT . '.tmp-' . getmypid();
|
|
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($json === false) jerr(500, 'JSON encode failed');
|
|
if (@file_put_contents($tmp, $json) === false) jerr(500, 'tmp write failed');
|
|
if (!@rename($tmp, $SSOT)) jerr(500, 'rename failed');
|
|
@chmod($SSOT, 0644);
|
|
|
|
logline("UPDATE agent=$agent fields=" . implode(',', array_keys($updates)) . " bak=" . basename($bakFile));
|
|
|
|
// Wave-277 bis: auto-purge CF cache for SSOT endpoints (zero manual purge)
|
|
// Doctrine: propagation instantanee (sinon cache max-age=30 retarde visuel)
|
|
$cfPurged = false;
|
|
$cfEmail = 'ymahboub@weval-consulting.com';
|
|
$cfKey = null;
|
|
if (is_readable('/etc/weval/secrets.env')) {
|
|
foreach (file('/etc/weval/secrets.env', FILE_IGNORE_NEW_LINES) as $line) {
|
|
if (strpos($line, 'CF_AI_KEY=') === 0) { $cfKey = trim(substr($line, 10)); break; }
|
|
}
|
|
}
|
|
if ($cfKey) {
|
|
$urlsToPurge = json_encode(['files' => [
|
|
'https://weval-consulting.com/api/agent-avatars.php',
|
|
'https://weval-consulting.com/api/agent-avatars-v75.php',
|
|
'https://weval-consulting.com/api/agent-avatars-v2.json'
|
|
]]);
|
|
$ch = curl_init('https://api.cloudflare.com/client/v4/zones/1488bbba251c6fa282999fcc09aac9fe/purge_cache');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $urlsToPurge,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 8,
|
|
CURLOPT_HTTPHEADER => [
|
|
'X-Auth-Email: ' . $cfEmail,
|
|
'X-Auth-Key: ' . $cfKey,
|
|
'Content-Type: application/json'
|
|
]
|
|
]);
|
|
$cfResp = curl_exec($ch);
|
|
curl_close($ch);
|
|
if ($cfResp) {
|
|
$cfData = json_decode($cfResp, true);
|
|
$cfPurged = $cfData['success'] ?? false;
|
|
logline('CF_PURGE agent=' . $agent . ' result=' . ($cfPurged ? 'OK' : 'FAIL'));
|
|
}
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'agent' => $agent,
|
|
'updated_fields' => array_keys($updates),
|
|
'cf_purged' => $cfPurged,
|
|
'before' => $before,
|
|
'after' => $after,
|
|
'backup' => basename($bakFile),
|
|
'total_agents' => count($data)
|
|
]);
|