Lizenz-Vertrieb Feinschliff: Zugabe +10%, sichere Datei-Einloesung, Mail-Config
Umsetzung des Thomas-Feedbacks: - Reihenfolge: freie Lizenzen beim Lehrer jetzt ORDER BY id (= Reihenfolge der reinkopierten Liste); stornierte ausgeblendet. - Gratis-Zugabe: Checkbox (Default an) "+10 % aufgerundet" -> geliefert = ceil(bestellt*1.1) (14 -> 16). Batch speichert qty_ordered (verrechnet) und qty (geliefert); Live-Vorschau im Formular. - Einloeselink/QR ENTFERNT (Sicherheitsrisiko: falscher eingeloggter Account). einloesen.html + pages/einloesen.php geloescht, Sammellink aus UI/Mail raus. - CSV: Gueltig-bis + Metadaten nur in der KOPFZEILE, danach reine Code-Zeilen (per Copy-Paste einloesbar). Dateiname: <ExtBestellnr|Datum>_geograsim_<intNr>. - Interne Bestellnummer = batch.id (laufend, nicht editierbar, storno-stabil). - Lehrer-Einloesen: sichere .txt/.csv-Ablage (mehrstufig: Endung+MIME, Groessen- limit, nur Text lesen, Whitelist-Regex extrahiert nur Code-Tokens, Server validiert) — kein Upload, keine Ausfuehrung/Rendering. - Mailsystem: zentrale Config in neuer app_settings (Provider server|aws, Absender-Name/-Adresse, oeffentliche URL), pflegbar in Admin + Test-Versand. Mailer liest Absender aus Config (SMTP-Zugang bleibt in .env). Mailtext nutzt kanonische geograsim.at (nicht v3/localhost). Migrationen: -2 (qty_ordered), -3 (app_settings). Beide additiv, auf v3 angewandt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+65
-11
@@ -87,6 +87,14 @@ function lic_attempt(Database $db, ?int $teacherId, string $code, bool $ok): voi
|
||||
[$ip !== '' ? $ip : null, $teacherId, substr($code, 0, 32), $ok ? 1 : 0]);
|
||||
}
|
||||
|
||||
/** App-Setting lesen (app_settings), robust mit Default. */
|
||||
function lic_cfg(Database $db, string $key, string $default = ''): string {
|
||||
try {
|
||||
$r = $db->fetchOne("SELECT sval FROM app_settings WHERE skey = ?", [$key]);
|
||||
return ($r && $r['sval'] !== null && $r['sval'] !== '') ? (string)$r['sval'] : $default;
|
||||
} catch (\Throwable $e) { return $default; }
|
||||
}
|
||||
|
||||
// === GET ===
|
||||
if ($method === 'GET') {
|
||||
// Super-Admin: alle Lizenzen
|
||||
@@ -105,6 +113,16 @@ if ($method === 'GET') {
|
||||
]);
|
||||
}
|
||||
|
||||
// Mail-System-Konfiguration
|
||||
if (isset($_GET['mail_config'])) {
|
||||
Response::ok([
|
||||
'provider' => lic_cfg($db, 'mail_provider', 'server'),
|
||||
'from_email' => lic_cfg($db, 'mail_from_email', 'support@geograsim.at'),
|
||||
'from_name' => lic_cfg($db, 'mail_from_name', 'GeoGraSim'),
|
||||
'public_url' => lic_cfg($db, 'mail_public_url', 'https://geograsim.at'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Kunden-Autocomplete
|
||||
if (isset($_GET['customers'])) {
|
||||
$like = '%' . trim($_GET['customers']) . '%';
|
||||
@@ -214,9 +232,10 @@ if ($method === 'GET') {
|
||||
[$classId]
|
||||
);
|
||||
|
||||
// Auch freie Lizenzen des Lehrers (noch nicht zugewiesen)
|
||||
// Auch freie Lizenzen des Lehrers (noch nicht zugewiesen), storniert ausgeschlossen.
|
||||
// Reihenfolge = id (= Reihenfolge, in der die Codes ausgegeben/reinkopiert wurden).
|
||||
$freeLicenses = $db->fetchAll(
|
||||
'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL ORDER BY code',
|
||||
'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL AND canceled_at IS NULL ORDER BY id',
|
||||
[$teacherId]
|
||||
);
|
||||
|
||||
@@ -348,8 +367,11 @@ if ($method === 'POST') {
|
||||
$adminId = Session::adminId();
|
||||
if (!$adminId) Response::error('Nur Admin', 403);
|
||||
|
||||
$count = (int)($body['count'] ?? 0);
|
||||
if ($count < 1 || $count > 5000) Response::error('Anzahl muss zwischen 1 und 5000 liegen');
|
||||
$ordered = (int)($body['count'] ?? 0);
|
||||
if ($ordered < 1 || $ordered > 5000) Response::error('Anzahl muss zwischen 1 und 5000 liegen');
|
||||
// Gratis-Zugabe: +10 %, aufgerundet (14 → 16). Standard aktiv.
|
||||
$bonus = !array_key_exists('bonus', $body) || !empty($body['bonus']);
|
||||
$count = $bonus ? (int)ceil($ordered * 1.1) : $ordered;
|
||||
$type = in_array($body['sales_type'] ?? '', LIC_TYPES, true) ? $body['sales_type'] : 'privat';
|
||||
$orderNo = trim((string)($body['order_no'] ?? '')) ?: null;
|
||||
$note = trim((string)($body['note'] ?? '')) ?: null;
|
||||
@@ -366,9 +388,9 @@ if ($method === 'POST') {
|
||||
$db->begin();
|
||||
try {
|
||||
$db->execute(
|
||||
"INSERT INTO license_batches (customer_id, order_no, sales_type, qty, default_valid_from, default_valid_until, note, redeem_token, created_by)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
[$customerId, $orderNo, $type, $count, $validFrom, $validUntil, $note, $token, $adminId]);
|
||||
"INSERT INTO license_batches (customer_id, order_no, sales_type, qty, qty_ordered, default_valid_from, default_valid_until, note, redeem_token, created_by)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
[$customerId, $orderNo, $type, $count, $ordered, $validFrom, $validUntil, $note, $token, $adminId]);
|
||||
$batchId = (int)$db->lastInsertId();
|
||||
|
||||
$codes = []; $made = 0; $tries = 0; $maxTries = $count * 8 + 50;
|
||||
@@ -383,16 +405,16 @@ if ($method === 'POST') {
|
||||
} catch (\Throwable $e) { /* Code-Kollision (extrem selten) → neuer Versuch */ }
|
||||
}
|
||||
if ($made < $count) throw new \RuntimeException("Nur $made von $count Codes erzeugt");
|
||||
lic_event($db, $batchId, null, 'issued', "$count Codes, gültig $validFrom…$validUntil, Typ $type" . ($orderNo ? ", Best.-Nr. $orderNo" : ''), $adminId);
|
||||
lic_event($db, $batchId, null, 'issued', "bestellt $ordered, geliefert $made (Zugabe " . ($bonus ? 'ja' : 'nein') . "), gültig $validFrom…$validUntil, Typ $type" . ($orderNo ? ", Best.-Nr. $orderNo" : ''), $adminId);
|
||||
$db->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$db->rollBack();
|
||||
Response::error('Fehler beim Erzeugen: ' . $e->getMessage(), 500);
|
||||
}
|
||||
Response::ok([
|
||||
'batch_id' => $batchId, 'redeem_token' => $token,
|
||||
'batch_id' => $batchId, 'internal_no' => $batchId, 'redeem_token' => $token,
|
||||
'valid_from' => $validFrom, 'valid_until' => $validUntil,
|
||||
'count' => $made, 'codes' => $codes,
|
||||
'ordered' => $ordered, 'count' => $made, 'codes' => $codes,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -444,6 +466,8 @@ if ($method === 'POST') {
|
||||
if ($action === 'send_batch_mail') {
|
||||
$adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403);
|
||||
$bid = (int)($body['batch_id'] ?? 0); if (!$bid) Response::error('batch_id erforderlich');
|
||||
$provider = lic_cfg($db, 'mail_provider', 'server');
|
||||
if ($provider !== 'server') Response::error('Aktiver Mail-Treiber „' . $provider . '" ist noch nicht verfügbar. In den Mail-Einstellungen auf „server" stellen.');
|
||||
$to = trim((string)($body['to_email'] ?? ''));
|
||||
$subject = trim((string)($body['subject'] ?? '')) ?: 'Ihre GeoGraSim-Lizenzcodes';
|
||||
$textBody = (string)($body['body_text'] ?? '');
|
||||
@@ -459,7 +483,7 @@ if ($method === 'POST') {
|
||||
}
|
||||
$db->execute(
|
||||
"INSERT INTO license_mails (batch_id, to_email, subject, body_text, body_html, provider, status, created_by) VALUES (?,?,?,?,?,?,?,?)",
|
||||
[$bid, $to, $subject, $textBody, $html, 'server', 'queued', $adminId]);
|
||||
[$bid, $to, $subject, $textBody, $html, $provider, 'queued', $adminId]);
|
||||
$mailId = (int)$db->lastInsertId();
|
||||
try {
|
||||
Mailer::sendWithAttachments($to, $subject, $html, $textBody, $attach);
|
||||
@@ -489,6 +513,36 @@ if ($method === 'POST') {
|
||||
Response::ok(['redeemed' => $n]);
|
||||
}
|
||||
|
||||
// ═══════════════ Mail-System-Konfiguration ═══════════════
|
||||
if ($action === 'set_mail_config') {
|
||||
$adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403);
|
||||
$prov = in_array($body['provider'] ?? '', ['server', 'aws'], true) ? $body['provider'] : 'server';
|
||||
$femail = trim((string)($body['from_email'] ?? ''));
|
||||
$fname = trim((string)($body['from_name'] ?? ''));
|
||||
$purl = rtrim(trim((string)($body['public_url'] ?? '')), '/');
|
||||
if ($femail !== '' && !filter_var($femail, FILTER_VALIDATE_EMAIL)) Response::error('Absender-Adresse ungültig');
|
||||
$set = ['mail_provider' => $prov, 'mail_from_email' => $femail, 'mail_from_name' => $fname, 'mail_public_url' => $purl];
|
||||
foreach ($set as $k => $v) {
|
||||
if ($v === '') continue;
|
||||
$db->execute("INSERT INTO app_settings (skey, sval, updated_by) VALUES (?,?,?) ON DUPLICATE KEY UPDATE sval = VALUES(sval), updated_by = VALUES(updated_by)", [$k, $v, $adminId]);
|
||||
}
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
if ($action === 'test_mail') {
|
||||
$adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403);
|
||||
$to = trim((string)($body['to_email'] ?? ''));
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) Response::error('Gültige Test-Adresse erforderlich');
|
||||
$provider = lic_cfg($db, 'mail_provider', 'server');
|
||||
if ($provider !== 'server') Response::error('Aktiver Treiber „' . $provider . '" ist noch nicht verfügbar. Auf „server" stellen.');
|
||||
try {
|
||||
Mailer::sendWithAttachments($to, 'GeoGraSim — Test-Mail',
|
||||
Mailer::renderPlainBody("Test-Mail aus der Lizenz-Verwaltung.\nAbsender und Versand funktionieren."),
|
||||
'Test-Mail — Versand funktioniert.');
|
||||
Response::ok(['status' => 'sent']);
|
||||
} catch (\Throwable $e) { Response::error('Versand fehlgeschlagen: ' . $e->getMessage(), 500); }
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
|
||||
+21
-4
@@ -16,13 +16,30 @@ class Mailer {
|
||||
$mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||
$mail->Port = defined('SMTP_PORT') ? (int)SMTP_PORT : 587;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->setFrom(
|
||||
defined('SMTP_FROM') ? SMTP_FROM : 'support@geograsim.at',
|
||||
defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'GeoGraSim'
|
||||
);
|
||||
[$fromEmail, $fromName] = self::configuredFrom();
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
/** Absender aus Admin-Config (platform_config_v2), Fallback auf .env-Konstanten.
|
||||
* SMTP-Zugangsdaten bleiben bewusst in .env (geheim) — nur die sichtbare
|
||||
* Absenderadresse/-name ist zentral in der Admin-Oberflaeche pflegbar. */
|
||||
private static function configuredFrom(): array {
|
||||
$email = defined('SMTP_FROM') ? SMTP_FROM : 'support@geograsim.at';
|
||||
$name = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'GeoGraSim';
|
||||
try {
|
||||
if (class_exists('Database')) {
|
||||
$rows = Database::get()->fetchAll(
|
||||
"SELECT skey, sval FROM app_settings WHERE skey IN ('mail_from_email','mail_from_name')");
|
||||
foreach ($rows as $row) {
|
||||
if ($row['skey'] === 'mail_from_email' && !empty($row['sval'])) $email = $row['sval'];
|
||||
if ($row['skey'] === 'mail_from_name' && !empty($row['sval'])) $name = $row['sval'];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) { /* Config nicht verfuegbar -> Defaults */ }
|
||||
return [$email, $name];
|
||||
}
|
||||
|
||||
public static function send(string $to, string $subject, string $htmlBody, string $textBody = ''): bool {
|
||||
try {
|
||||
$mail = self::create();
|
||||
|
||||
Reference in New Issue
Block a user