Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) session_start();
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../public/includes/init_pr_cl.php';
$PUSHCFG = require __DIR__ . '/config.php';
function json_response(array $data, int $status = 200): never {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Auth JSON (pas de redirect HTML)
*/
function require_auth_json(?array $roles = null): void {
if (!is_logged_in()) {
json_response(['ok'=>false, 'error'=>'auth_required'], 401);
}
if ($roles) {
$role = strtolower((string)($_SESSION['role'] ?? ''));
$allowed = array_map(fn($r)=>strtolower((string)$r), $roles);
if (!in_array($role, $allowed, true)) {
json_response(['ok'=>false, 'error'=>'forbidden'], 403);
}
}
}
function read_json_body(): array {
$raw = file_get_contents('php://input');
if (!$raw) return [];
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/_bootstrap.php';
require_auth_json(['client','praticien','secretaire','admin']);
$compteId = (int)($_SESSION['compte_id'] ?? 0);
if ($compteId <= 0) json_response(['ok'=>false,'error'=>'auth_required'], 401);
$stmt = $pdo->prepare("SELECT msg_unread, rdv_unread FROM push_badge_counters WHERE compte_id = ?");
$stmt->execute([$compteId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: ['msg_unread'=>0,'rdv_unread'=>0];
$msg = (int)($row['msg_unread'] ?? 0);
$rdv = (int)($row['rdv_unread'] ?? 0);
json_response([
'ok' => true,
'msg_unread' => $msg,
'rdv_unread' => $rdv,
'total' => $msg + $rdv,
]);
+14
View File
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
return [
// Génère tes clés VAPID et colle-les ici
'vapid_public' => getenv('PUSH_VAPID_PUBLIC') ?: '',
'vapid_private' => getenv('PUSH_VAPID_PRIVATE') ?: '',
// Obligatoire pour VAPID (email ou URL)
'vapid_subject' => 'mailto:contact@uslugi.fr',
// TTL push (secondes)
'ttl' => 60 * 60 * 24, // 24h
];
View File
Whitespace-only changes.
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
// Log erreurs PHP (utile sans SSH)
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/webcron_error.log');
// ===== sécurité =====
const CRON_TOKEN = 'e3e6ba5785a447c3f763a4ca4b8c36480df63a19';
if (!hash_equals(CRON_TOKEN, $_GET['token'] ?? '')) {
http_response_code(403);
exit("forbidden\n");
}
// ===== anti double exécution =====
$lockFile = __DIR__ . '/cron.lock';
$lock = fopen($lockFile, 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
exit("already running\n");
}
// ===== log minimal =====
file_put_contents(__DIR__ . '/webcron.log', date('Y-m-d H:i:s') . " run\n", FILE_APPEND);
// ===== lance ton worker =====
require __DIR__ . '/worker.php';
echo "OK\n";
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/_bootstrap.php';
// pas besoin d’être connecté pour lire la clé publique
json_response([
'ok' => true,
'publicKey' => $PUSHCFG['vapid_public'] ?? '',
]);
+64
View File
@@ -0,0 +1,64 @@
<?php
// /push/push_endpoint.php
declare(strict_types=1);
function push_queue_notification(
PDO $pdo,
string $targetRole,
?int $targetReferenceId,
?int $targetCompteId,
string $type,
string $title,
string $body,
?string $url = null,
?array $data = null
): ?int {
// Sécurité / hygiène
$title = trim($title);
$body = trim($body);
if ($title === '' || $body === '') return null;
$dataJson = $data ? json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
$stmt = $pdo->prepare("
INSERT INTO push_notifications
(target_role, target_reference_id, target_compte_id, type, title, body, url, data_json, status, attempts, created_at)
VALUES
(:role, :ref_id, :compte_id, :type, :title, :body, :url, :data_json, 'pending', 0, NOW())
");
$stmt->execute([
':role' => $targetRole,
':ref_id' => $targetReferenceId,
':compte_id' => $targetCompteId,
':type' => $type,
':title' => mb_substr($title, 0, 120),
':body' => mb_substr($body, 0, 255),
':url' => $url ? mb_substr($url, 0, 255) : null,
':data_json' => $dataJson,
]);
return (int)$pdo->lastInsertId();
}
/**
* Helpers “RDV” : cible métier par reference_id (client_id / praticien_id)
*/
function resolve_compte_id(PDO $pdo, string $role, int $refId): int {
$st = $pdo->prepare("SELECT id FROM compte WHERE role = ? AND reference_id = ? LIMIT 1");
$st->execute([$role, $refId]);
return (int)($st->fetchColumn() ?: 0);
}
function push_rdv_to_client(PDO $pdo, int $clientId, string $title, string $body, string $url, array $data = []): ?int {
$cid = resolve_compte_id($pdo, 'client', $clientId);
return push_queue_notification($pdo, 'client', $clientId, ($cid>0?$cid:null), 'rdv', $title, $body, $url, $data);
}
function push_rdv_to_praticien(PDO $pdo, int $praticienId, string $title, string $body, string $url, array $data = []): ?int {
$cid = resolve_compte_id($pdo, 'praticien', $praticienId);
return push_queue_notification($pdo, 'praticien', $praticienId, ($cid>0?$cid:null), 'rdv', $title, $body, $url, $data);
}
function push_rdv_to_secretaire(PDO $pdo, int $secretaireCompteId, string $title, string $body, string $url, array $data = []): ?int {
return push_queue_notification($pdo, 'secretaire', null, $secretaireCompteId, 'rdv', $title, $body, $url, $data);
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
function push_queue_notification(
PDO $pdo,
array $target, // ['compte_id'=>?, 'role'=>?, 'reference_id'=>?] (tu peux passer 1 ou 2 champs)
string $type,
string $title,
string $body,
?string $url = null,
?array $data = null
): int {
$stmt = $pdo->prepare("
INSERT INTO push_notifications
(target_role, target_reference_id, target_compte_id, type, title, body, url, data_json, status, created_at)
VALUES
(:role, :ref, :cid, :type, :title, :body, :url, :data, 'pending', NOW())
");
$stmt->execute([
':role' => (string)($target['role'] ?? 'client'),
':ref' => isset($target['reference_id']) ? (int)$target['reference_id'] : null,
':cid' => isset($target['compte_id']) ? (int)$target['compte_id'] : null,
':type' => $type,
':title'=> $title,
':body' => $body,
':url' => $url,
':data' => $data ? json_encode($data, JSON_UNESCAPED_UNICODE) : null,
]);
return (int)$pdo->lastInsertId();
}
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/_bootstrap.php';
require_auth_json(['client','praticien','secretaire','admin']);
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
json_response(['ok'=>false,'error'=>'method_not_allowed'], 405);
}
$input = read_json_body();
// Format attendu: { endpoint, keys:{p256dh,auth}, userAgent?, deviceLabel? }
$endpoint = trim((string)($input['endpoint'] ?? ''));
$keys = $input['keys'] ?? [];
$p256dh = trim((string)($keys['p256dh'] ?? ''));
$auth = trim((string)($keys['auth'] ?? ''));
if ($endpoint === '' || $p256dh === '' || $auth === '') {
json_response(['ok'=>false,'error'=>'bad_params'], 400);
}
$compteId = (int)($_SESSION['compte_id'] ?? 0);
$role = strtolower((string)($_SESSION['role'] ?? ''));
$referenceId = $_SESSION['reference_id'] ?? null;
$userAgent = trim((string)($input['userAgent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? '')));
$deviceLabel = trim((string)($input['deviceLabel'] ?? ''));
try {
// Upsert par endpoint (UNIQUE endpoint(255))
$sql = "
INSERT INTO push_subscriptions
(compte_id, role, reference_id, endpoint, p256dh, auth, user_agent, device_label, is_active, last_seen_at)
VALUES
(:compte_id, :role, :reference_id, :endpoint, :p256dh, :auth, :ua, :label, 1, NOW())
ON DUPLICATE KEY UPDATE
compte_id = VALUES(compte_id),
role = VALUES(role),
reference_id = VALUES(reference_id),
p256dh = VALUES(p256dh),
auth = VALUES(auth),
user_agent = VALUES(user_agent),
device_label = VALUES(device_label),
is_active = 1,
last_seen_at = NOW()
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':compte_id' => $compteId,
':role' => $role,
':reference_id' => $referenceId !== null ? (int)$referenceId : null,
':endpoint' => $endpoint,
':p256dh' => $p256dh,
':auth' => $auth,
':ua' => $userAgent !== '' ? $userAgent : null,
':label' => $deviceLabel !== '' ? $deviceLabel : null,
]);
json_response(['ok'=>true]);
} catch (Throwable $e) {
error_log('push subscribe error: '.$e->getMessage());
json_response(['ok'=>false,'error'=>'server_error'], 500);
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/_bootstrap.php';
require_auth_json(['client','praticien','secretaire','admin']);
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
json_response(['ok'=>false,'error'=>'method_not_allowed'], 405);
}
$input = read_json_body();
$endpoint = trim((string)($input['endpoint'] ?? ''));
if ($endpoint === '') {
json_response(['ok'=>false,'error'=>'bad_params'], 400);
}
try {
$stmt = $pdo->prepare("
UPDATE push_subscriptions
SET is_active = 0, updated_at = NOW()
WHERE endpoint = :e
AND compte_id = :cid
");
$stmt->execute([
':e' => $endpoint,
':cid' => (int)($_SESSION['compte_id'] ?? 0),
]);
json_response(['ok'=>true]);
} catch (Throwable $e) {
error_log('push unsubscribe error: '.$e->getMessage());
json_response(['ok'=>false,'error'=>'server_error'], 500);
}
+20980
View File
File diff suppressed because it is too large. Load diff
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../includes/bootstrap_global.php';
require_once __DIR__ . '/../vendor/autoload.php';
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
$cfg = require __DIR__ . '/config.php';
$auth = [
'VAPID' => [
'subject' => $cfg['vapid_subject'],
'publicKey' => $cfg['vapid_public'],
'privateKey' => $cfg['vapid_private'],
],
];
$webPush = new WebPush($auth);
$webPush->setDefaultOptions([
'TTL' => (int)($cfg['ttl'] ?? 86400),
]);
/**
* badgeCount = msg_unread + rdv_unread
* table push_badge_counters est indexée par compte_id (PK)
*/
function computeBadgeCount(PDO $pdo, array $n): int
{
$cid = (int)($n['target_compte_id'] ?? 0);
if ($cid <= 0) return 0;
$st = $pdo->prepare("SELECT msg_unread, rdv_unread FROM push_badge_counters WHERE compte_id = ? LIMIT 1");
$st->execute([$cid]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if (!$row) return 0;
$total = (int)($row['msg_unread'] ?? 0) + (int)($row['rdv_unread'] ?? 0);
return max(0, min(99, $total));
}
// 1) lot pending
$notifs = $pdo->query("
SELECT *
FROM push_notifications
WHERE status = 'pending'
ORDER BY id ASC
LIMIT 50
")->fetchAll(PDO::FETCH_ASSOC);
if (!$notifs) exit(0);
foreach ($notifs as $n) {
$nid = (int)$n['id'];
// 1bis) lock
$lock = $pdo->prepare("
UPDATE push_notifications
SET status='sending'
WHERE id=? AND status='pending'
");
$lock->execute([$nid]);
if ($lock->rowCount() === 0) continue;
// 2) subscriptions cibles
$where = ["is_active = 1"];
$params = [];
if (!empty($n['target_compte_id'])) {
$where[] = "compte_id = :cid";
$params[':cid'] = (int)$n['target_compte_id'];
} else {
// fallback (normalement tu as toujours target_compte_id)
$where[] = "role = :role";
$params[':role'] = (string)$n['target_role'];
if (!empty($n['target_reference_id'])) {
$where[] = "reference_id = :ref";
$params[':ref'] = (int)$n['target_reference_id'];
}
}
$stmt = $pdo->prepare("
SELECT id, endpoint, p256dh, auth
FROM push_subscriptions
WHERE " . implode(' AND ', $where) . "
");
$stmt->execute($params);
$subs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!$subs) {
$pdo->prepare("
UPDATE push_notifications
SET status='failed', attempts=attempts+1, last_error='no_subscriptions'
WHERE id=?
")->execute([$nid]);
continue;
}
// 3) payload
$badgeCount = computeBadgeCount($pdo, $n);
$payload = [
'type' => $n['type'],
'title' => $n['title'],
'body' => $n['body'],
'url' => $n['url'],
'data' => $n['data_json'] ? json_decode((string)$n['data_json'], true) : null,
'nid' => $nid,
'badgeCount' => $badgeCount,
];
$payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// map endpoint => subscription_id
$subIdByEndpoint = [];
foreach ($subs as $s) {
$endpoint = (string)$s['endpoint'];
$subIdByEndpoint[$endpoint] = (int)$s['id'];
$subscription = Subscription::create([
'endpoint' => $endpoint,
'publicKey' => (string)$s['p256dh'],
'authToken' => (string)$s['auth'],
]);
$webPush->queueNotification($subscription, $payloadJson, ['urgency' => 'normal']);
}
// 4) flush + delivery log
$successCount = 0;
$lastErr = null;
$insLog = $pdo->prepare("
INSERT INTO push_delivery_log
(notification_id, subscription_id, http_status, error_code, error_message, created_at)
VALUES
(:nid, :sid, :http, :ecode, :emsg, NOW())
");
foreach ($webPush->flush() as $report) {
$ok = $report->isSuccess();
$endpoint = (string)$report->getRequest()->getUri();
$sid = $subIdByEndpoint[$endpoint] ?? null;
$statusCode = $report->getResponse() ? $report->getResponse()->getStatusCode() : null;
$reason = $ok ? null : (string)$report->getReason();
if ($ok) $successCount++;
if ($sid) {
$insLog->execute([
':nid' => $nid,
':sid' => $sid,
':http' => $statusCode,
':ecode' => (!$ok && in_array((int)$statusCode, [404, 410], true)) ? 'gone' : null,
':emsg' => $reason ? mb_substr($reason, 0, 255) : null,
]);
}
if (!$ok && in_array((int)$statusCode, [404, 410], true)) {
$pdo->prepare("UPDATE push_subscriptions SET is_active=0 WHERE endpoint=?")->execute([$endpoint]);
}
if (!$ok && $reason) $lastErr = $reason;
}
// 5) statut notification
if ($successCount > 0) {
$pdo->prepare("
UPDATE push_notifications
SET status='sent', sent_at=NOW(), attempts=attempts+1, last_error=NULL
WHERE id=?
")->execute([$nid]);
} else {
$pdo->prepare("
UPDATE push_notifications
SET status='failed', attempts=attempts+1, last_error=?
WHERE id=?
")->execute([mb_substr((string)$lastErr, 0, 255), $nid]);
}
}