eplan/ajax/pwa_api.php
Eduard Wisch 593c93f377
All checks were successful
Deploy Eplan / deploy (push) Successful in 9s
feat: PWA-Token-Auth statt Dolibarr-REST-API-Key [deploy]
Das ElektroPlan-Backend spricht nicht mehr die Dolibarr-REST-API an
(API-Key-Flow kaputt, REST-Modul nicht zuverlässig), sondern einen
eigenen NOLOGIN-Endpoint im Eplan-Modul.

- ajax/pwa_api.php: NOLOGIN-Endpoint, Auth via X-Eplan-Token-Header
  gegen EPLAN_PWA_SECRET (hash_equals, timing-safe)
  Actions: ping, auftraege_listen, auftrag_details, kunden_suchen,
  dokument_upload (multipart POST)
- lib/eplan_token.lib.php: Lazy-init Token, Rotation, Verifikation
- admin/setup.php: Token-Feld (readonly, anklickbar zum Markieren),
  Endpoint-URL zum Kopieren, Button "Neuen Token erzeugen"
- de/en-Sprachdateien: EplanApiToken* Keys

Muster aus KB #354 (Bericht/Baustelle-PWA).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:02:27 +02:00

175 lines
7.9 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
* GPL v3+
*
* NOLOGIN-Endpoint für die ElektroPlan-PWA / FastAPI-Backend.
* Auth: Header "X-Eplan-Token: <secret>" gegen Const EPLAN_PWA_SECRET (hash_equals).
*
* Actions (?action=...):
* - ping → {aktiv: true, version: "1.1.0"}
* - auftraege_listen → Liste offener Aufträge (Commande)
* - auftrag_details&id= → Einzelner Auftrag mit Positionen
* - kunden_suchen&such= → Third-Party-Suche
* - dokument_upload (POST) → Plan-PDF an Auftrag/Kunde anhängen
*/
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1');
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
if (!defined('NOLOGIN')) define('NOLOGIN', '1');
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; }
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) { http_response_code(500); echo json_encode(['error' => 'main.inc.php not found']); exit; }
dol_include_once('/eplan/lib/eplan_token.lib.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
// --- Auth ---
$token = eplanTokenAusRequest();
if (!eplanTokenPruefen($token)) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized', 'detail' => 'Ungültiger oder fehlender Token']);
exit;
}
if (!isModEnabled('eplan')) {
http_response_code(503);
echo json_encode(['error' => 'Eplan-Modul deaktiviert']);
exit;
}
$action = GETPOST('action', 'aZ09');
try {
switch ($action) {
case 'ping':
echo json_encode([
'aktiv' => true,
'modul' => 'eplan',
'version' => '1.2.0',
'dolibarr' => DOL_VERSION,
]);
break;
case 'auftraege_listen':
$such = GETPOST('such', 'alphanohtml');
$limit = max(1, min(200, (int) GETPOST('limit', 'int') ?: 50));
$sql = "SELECT c.rowid as id, c.ref, c.ref_client, c.date_commande, c.total_ttc, c.fk_statut,
t.nom as kunde
FROM ".MAIN_DB_PREFIX."commande c
LEFT JOIN ".MAIN_DB_PREFIX."societe t ON t.rowid = c.fk_soc
WHERE c.entity IN (".getEntity('commande').")";
if (!empty($such)) {
$such_esc = $db->escape($such);
$sql .= " AND (c.ref LIKE '%".$such_esc."%'
OR c.ref_client LIKE '%".$such_esc."%'
OR t.nom LIKE '%".$such_esc."%')";
}
$sql .= " ORDER BY c.date_commande DESC LIMIT ".((int) $limit);
$resql = $db->query($sql);
$liste = [];
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$liste[] = [
'id' => (int) $obj->id,
'ref' => $obj->ref,
'ref_client' => $obj->ref_client,
'kunde' => $obj->kunde,
'status' => (int) $obj->fk_statut,
'datum' => $obj->date_commande,
'total_ttc' => $obj->total_ttc !== null ? (float) $obj->total_ttc : null,
];
}
$db->free($resql);
}
echo json_encode($liste);
break;
case 'auftrag_details':
$id = (int) GETPOST('id', 'int');
if ($id <= 0) { http_response_code(400); echo json_encode(['error' => 'id fehlt']); break; }
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
$c = new Commande($db);
if ($c->fetch($id) <= 0) { http_response_code(404); echo json_encode(['error' => 'nicht gefunden']); break; }
$c->fetch_thirdparty();
echo json_encode([
'id' => (int) $c->id,
'ref' => $c->ref,
'ref_client' => $c->ref_client,
'kunde' => $c->thirdparty ? $c->thirdparty->name : null,
'kunde_id' => $c->thirdparty ? (int) $c->thirdparty->id : null,
'datum' => $c->date_commande,
'status' => (int) $c->statut,
'total_ttc' => (float) $c->total_ttc,
]);
break;
case 'kunden_suchen':
$such = GETPOST('such', 'alphanohtml');
if (empty($such) || strlen($such) < 2) { echo json_encode([]); break; }
$such_esc = $db->escape($such);
$sql = "SELECT rowid as id, nom, code_client FROM ".MAIN_DB_PREFIX."societe
WHERE entity IN (".getEntity('societe').")
AND (nom LIKE '%".$such_esc."%' OR code_client LIKE '%".$such_esc."%')
ORDER BY nom LIMIT 30";
$resql = $db->query($sql);
$liste = [];
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$liste[] = ['id' => (int) $obj->id, 'name' => $obj->nom, 'code' => $obj->code_client];
}
$db->free($resql);
}
echo json_encode($liste);
break;
case 'dokument_upload':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); echo json_encode(['error' => 'POST benötigt']); break;
}
$auftrag_id = (int) GETPOST('auftrag_id', 'int');
if ($auftrag_id <= 0) { http_response_code(400); echo json_encode(['error' => 'auftrag_id fehlt']); break; }
if (empty($_FILES['datei'])) { http_response_code(400); echo json_encode(['error' => 'Keine Datei']); break; }
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
$c = new Commande($db);
if ($c->fetch($auftrag_id) <= 0) { http_response_code(404); echo json_encode(['error' => 'Auftrag nicht gefunden']); break; }
$ref_sanitiert = dol_sanitizeFileName($c->ref);
$ziel_dir = $conf->commande->dir_output.'/'.$ref_sanitiert;
dol_mkdir($ziel_dir);
$dateiname = dol_sanitizeFileName($_FILES['datei']['name']);
$ziel = $ziel_dir.'/'.$dateiname;
$erfolg = dol_move_uploaded_file($_FILES['datei']['tmp_name'], $ziel, 1, 0, 0);
if ($erfolg > 0) {
echo json_encode(['erfolg' => true, 'pfad' => $ref_sanitiert.'/'.$dateiname]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Upload fehlgeschlagen', 'code' => $erfolg]);
}
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Unbekannte Action', 'action' => $action]);
}
} catch (Throwable $e) {
dol_syslog('eplan/pwa_api: '.$e->getMessage(), LOG_ERR);
http_response_code(500);
echo json_encode(['error' => 'Server-Fehler', 'detail' => $e->getMessage()]);
}
$db->close();