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:
+35
-2
@@ -431,8 +431,12 @@ schueler01,Max M.,,,,,</pre>
|
||||
<div class="modal hidden" id="modal-license">
|
||||
<div class="modal-card">
|
||||
<h3>🔑 Lizenzen einlösen</h3>
|
||||
<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.5rem">Ein oder mehrere Codes (einer pro Zeile):</p>
|
||||
<textarea id="ml-codes" rows="4" style="font-family:'Courier New',monospace" placeholder="ABCD-EFGH-JKLM-2026"></textarea>
|
||||
<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.5rem">Ein oder mehrere Codes (einer pro Zeile) — oder eine .txt/.csv-Datei ablegen:</p>
|
||||
<textarea id="ml-codes" rows="4" style="font-family:'Courier New',monospace" placeholder="ABCD-EFGH-JKLM-NPQR-STUV"></textarea>
|
||||
<div id="ml-drop" style="margin-top:.4rem;border:1.5px dashed #cbd8dc;border-radius:8px;padding:.5rem;text-align:center;font-size:.66rem;color:#8a8a8a;cursor:pointer" ondragover="event.preventDefault();this.style.background='#eef6f8'" ondragleave="this.style.background=''" ondrop="licenseDrop(event)">
|
||||
📄 .txt/.csv hierher ziehen oder <label style="color:#4a7c8a;font-weight:700;cursor:pointer;text-decoration:underline">auswählen<input type="file" id="ml-file" accept=".txt,.csv,text/plain,text/csv" style="display:none" onchange="licenseFilePick(this)"></label>
|
||||
<div id="ml-fileinfo" style="margin-top:.2rem"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.4rem;margin-top:.5rem">
|
||||
<button class="btn" onclick="redeemLicenses()">Einlösen</button>
|
||||
<button class="btn btn-outline" onclick="hideModal('modal-license')">Abbrechen</button>
|
||||
@@ -5512,6 +5516,35 @@ async function assignLicense(studentId) {
|
||||
loadLicenses();
|
||||
}
|
||||
|
||||
// Sicherer Datei-Import fuer Codes — mehrstufig:
|
||||
// (1) Endung+MIME .txt/.csv, (2) Groessenlimit, (3) NUR als Text lesen,
|
||||
// (4) Whitelist-Regex extrahiert ausschliesslich Code-Tokens (nichts wird
|
||||
// interpretiert/gerendert/ausgefuehrt), (5) Server validiert jeden Code.
|
||||
function extractCodes(text){
|
||||
var m = String(text).toUpperCase().match(/\b[A-Z0-9]{4}(?:-[A-Z0-9]{4}){2,4}\b/g) || [];
|
||||
var seen={}, out=[];
|
||||
for (var i=0;i<m.length;i++){ var c=m[i]; if(!seen[c]){ seen[c]=1; out.push(c); } if(out.length>=5000) break; }
|
||||
return out;
|
||||
}
|
||||
function ingestLicenseFile(file){
|
||||
var info=document.getElementById('ml-fileinfo'); if(!file) return;
|
||||
var name=(file.name||'').toLowerCase();
|
||||
if(!/\.(txt|csv)$/.test(name) || (file.type && !/^text\//.test(file.type))){
|
||||
info.innerHTML='<span style="color:#c07a6b">Nur .txt oder .csv erlaubt.</span>'; return; }
|
||||
if(file.size > 256*1024){ info.innerHTML='<span style="color:#c07a6b">Datei zu groß (max 256 KB).</span>'; return; }
|
||||
var reader=new FileReader();
|
||||
reader.onload=function(){
|
||||
var codes=extractCodes(reader.result);
|
||||
if(!codes.length){ info.innerHTML='<span style="color:#c07a6b">Keine gültigen Codes gefunden.</span>'; return; }
|
||||
document.getElementById('ml-codes').value=codes.join('\n');
|
||||
info.innerHTML='<span style="color:#5a8a5e">'+codes.length+' Codes aus Datei übernommen.</span>';
|
||||
};
|
||||
reader.onerror=function(){ info.innerHTML='<span style="color:#c07a6b">Datei konnte nicht gelesen werden.</span>'; };
|
||||
reader.readAsText(file);
|
||||
}
|
||||
function licenseFilePick(input){ if(input.files && input.files[0]) ingestLicenseFile(input.files[0]); input.value=''; }
|
||||
function licenseDrop(ev){ ev.preventDefault(); if(ev.currentTarget) ev.currentTarget.style.background=''; var f=ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0]; if(f) ingestLicenseFile(f); }
|
||||
|
||||
async function redeemLicenses() {
|
||||
var text = document.getElementById('ml-codes').value.trim();
|
||||
if (!text) return;
|
||||
|
||||
Reference in New Issue
Block a user