497 lines
24 KiB
PHP
497 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* View-Template zur Erstellung von Active-Directory-Benutzern.
|
|
*
|
|
* Funktionen:
|
|
* - Formular zum Anlegen eines einzelnen Benutzers (sAMAccountName, Anzeigename, E-Mail, Passwort, OU, Gruppen).
|
|
* - Formular zum Hochladen einer CSV-Datei zum Anlegen mehrerer Benutzer.
|
|
* - Vorschau-Textbox, in der die CSV-Datei angezeigt und bearbeitet werden kann, bevor sie abgesendet wird.
|
|
* - Gibt optionalen Erfolg / Fehler aus.
|
|
*
|
|
* Erwartete View-Daten:
|
|
* - string|null $error Fehlermeldung
|
|
* - string|null $success Erfolgsmeldung
|
|
*/
|
|
|
|
/**
|
|
* @var string|null $error
|
|
* @var string|null $success
|
|
*/
|
|
?>
|
|
|
|
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
|
<h1 class="h3 mb-0 text-gray-800">Benutzer erstellen</h1>
|
|
</div>
|
|
|
|
<?php if (!empty($error)): ?>
|
|
<div class="alert alert-danger" role="alert">
|
|
<?php echo htmlspecialchars((string)$error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($success)): ?>
|
|
<div class="alert alert-success" role="alert">
|
|
<?php echo htmlspecialchars((string)$success, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($powershellDryRun) && $powershellDryRun === true): ?>
|
|
<div class="alert alert-warning" role="alert">
|
|
Die Anwendung ist im <strong>Dry-Run</strong>-Modus konfiguriert; PowerShell-Befehle werden nicht ausgeführt.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<p class="mb-4">Hier können Sie einzelne Active-Directory-Benutzer anlegen oder eine CSV-Datei hochladen, um mehrere Benutzer gleichzeitig zu erstellen. Sie können die CSV in der Vorschau bearbeiten bevor Sie die Erstellung auslösen.</p>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-6">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Einzelner Benutzer</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" action="/api/create_user.php">
|
|
<div class="form-group">
|
|
<label for="samaccountname">Anmeldename (sAMAccountName)</label>
|
|
<input type="text" class="form-control" id="samaccountname" name="samaccountname" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="displayname">Anzeigename</label>
|
|
<input type="text" class="form-control" id="displayname" name="displayname">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mail">E-Mail</label>
|
|
<input type="email" class="form-control" id="mail" name="mail">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Passwort</label>
|
|
<input type="password" class="form-control" id="password" name="password" required>
|
|
<small class="form-text text-muted">Das Passwort muss mindestens 7 Zeichen lang sein, darf keine größeren Teile des Benutzernamens enthalten und muss Zeichen aus mindestens 3 von 4 Kategorien enthalten: Großbuchstaben, Kleinbuchstaben, Ziffern, Sonderzeichen.</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="groups">Gruppen (kommagetrennt, optional)</label>
|
|
<input type="text" class="form-control" id="groups" name="groups" placeholder="Domain Users,IT-Staff">
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">Benutzer erstellen</button>
|
|
</form>
|
|
<script>
|
|
(function () {
|
|
const form = document.querySelector('form[action="/api/create_user.php"]');
|
|
if (!form) return;
|
|
function validatePassword(password, sam) {
|
|
const errors = [];
|
|
if (!password || password.length < 7) errors.push('Passwort muss mindestens 7 Zeichen lang sein.');
|
|
let categories = 0;
|
|
if (/[A-Z]/.test(password)) categories++;
|
|
if (/[a-z]/.test(password)) categories++;
|
|
if (/\d/.test(password)) categories++;
|
|
if (/[^A-Za-z0-9]/.test(password)) categories++;
|
|
if (categories < 3) errors.push('Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten.');
|
|
if (sam) {
|
|
const pwLower = password.toLowerCase();
|
|
const samLower = sam.toLowerCase();
|
|
if (pwLower.includes(samLower)) errors.push('Passwort darf den Benutzernamen nicht enthalten.');
|
|
else {
|
|
const minLen = 4;
|
|
if (samLower.length >= minLen) {
|
|
outer: for (let len = minLen; len <= samLower.length; len++) {
|
|
for (let s = 0; s <= samLower.length - len; s++) {
|
|
const sub = samLower.substr(s, len);
|
|
if (pwLower.includes(sub)) { errors.push('Passwort darf keine größeren Teile des Benutzernamens enthalten.'); break outer; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
form.addEventListener('submit', function (e) {
|
|
const sam = document.getElementById('samaccountname').value.trim();
|
|
const password = document.getElementById('password').value;
|
|
const errs = validatePassword(password, sam);
|
|
if (errs.length > 0) {
|
|
e.preventDefault();
|
|
alert('Passwort ungültig:\n' + errs.join('\n'));
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
})();
|
|
</script>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Mehrere Benutzer via CSV</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="small text-muted">Die CSV-Datei sollte eine Kopfzeile mit folgenden Spalten enthalten: <code>samaccountname,displayname,mail,password,groups</code>. Gruppen können komma-getrennt sein. Nach dem Hochladen erscheint der Inhalt in der Vorschau, dort kann er vor dem Absenden editiert werden.</p>
|
|
|
|
<form id="csvUploadForm" method="post" action="/api/create_users_csv.php" enctype="multipart/form-data">
|
|
<div class="form-group">
|
|
<label for="csvfile">CSV-Datei</label>
|
|
<input type="file" class="form-control-file" id="csvfile" name="csvfile" accept=".csv">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="csvdelimiter">Trennzeichen</label>
|
|
<select class="form-control" id="csvdelimiter" name="csvdelimiter">
|
|
<option value="," selected>Komma (,)</option>
|
|
<option value=";">Semikolon (;)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="hasHeader">Kopfzeile vorhanden?</label>
|
|
<select class="form-control" id="hasHeader" name="hasHeader">
|
|
<option value="1" selected>Ja</option>
|
|
<option value="0">Nein</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="csvpreview">CSV-Vorschau (editierbar)</label>
|
|
<textarea id="csvpreview" name="csvcontent" rows="12" class="form-control" placeholder="CSV-Inhalt wird hier angezeigt, nachdem Sie eine Datei ausgewählt haben. Sie können den Text bearbeiten, bevor Sie ihn absenden."></textarea>
|
|
</div>
|
|
|
|
<!-- CSV preview table and validation info -->
|
|
<div id="csvPreviewArea" style="display:none; margin-top:1rem;">
|
|
<div id="csvPreviewInfo" class="small text-muted mb-2">CSV-Vorschau geladen. Passwörter werden geprüft.</div>
|
|
<div style="overflow-x:auto; max-height:260px;">
|
|
<table id="csvPreviewTable" class="table table-sm table-bordered" style="width:100%; border-collapse:collapse;"></table>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Make the CSV action buttons equal width and aligned */
|
|
.csv-btn-row { display:flex; gap:0.5rem; align-items:center; }
|
|
.csv-btn-row .btn-equal { min-width:150px; display:inline-flex; justify-content:center; align-items:center; }
|
|
.csv-preview-hint { margin-top:0.6rem; font-size:0.9rem; color:#6c757d; }
|
|
</style>
|
|
<div class="form-group">
|
|
<div class="csv-btn-row">
|
|
<button type="button" id="loadPreviewBtn" class="btn btn-secondary btn-equal" title="Hinweis: Beim Laden der Vorschau werden Änderungen in der Vorschau verworfen und mit der Originaldatei ersetzt.">In Vorschau laden</button>
|
|
<button type="submit" class="btn btn-primary btn-equal">CSV verarbeiten</button>
|
|
<button type="button" id="clearPreviewBtn" class="btn btn-light btn-equal">Vorschau löschen</button>
|
|
</div>
|
|
<div class="csv-preview-hint">Beim Laden werden Änderungen in der Vorschau verworfen und die Originaldatei neu eingelesen.</div>
|
|
</div>
|
|
</form>
|
|
|
|
<small class="form-text text-muted mt-2">Tipp: Wenn Sie die CSV im Textfeld bearbeiten, wird der bearbeitete Text an den Server gesendet.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($csvDetails) && is_array($csvDetails)): ?>
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-secondary">CSV Verarbeitungsergebnisse</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Anmeldename</th>
|
|
<th>Status</th>
|
|
<th>Hinweis</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($csvDetails as $detail): ?>
|
|
<tr>
|
|
<td><?php echo htmlspecialchars((string)($detail['sam'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
|
|
<td><?php echo (!empty($detail['success'])) ? 'OK' : 'FEHLER'; ?></td>
|
|
<td><?php echo htmlspecialchars((string)($detail['message'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-secondary">Hinweise</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul>
|
|
<li>Die tatsächliche Erstellung von AD-Benutzern wird serverseitig durchgeführt. Diese View sendet Daten an die Endpunkte <code>/api/create_user.php</code> und <code>/api/create_users_csv.php</code>.</li>
|
|
<li>Stellen Sie sicher, dass der Webserver die nötigen Rechte hat und die LDAP/AD-Verbindung korrekt konfiguriert ist.</li>
|
|
<li>Für Sicherheit: prüfen Sie bitte CSRF-Schutz und Validierung auf der Serverseite.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const fileInput = document.getElementById('csvfile');
|
|
const preview = document.getElementById('csvpreview');
|
|
const loadBtn = document.getElementById('loadPreviewBtn');
|
|
const clearBtn = document.getElementById('clearPreviewBtn');
|
|
const form = document.getElementById('csvUploadForm');
|
|
|
|
function readFileToPreview(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
preview.value = e.target.result || '';
|
|
};
|
|
reader.onerror = function () {
|
|
alert('Fehler beim Lesen der Datei.');
|
|
};
|
|
reader.readAsText(file, 'utf-8');
|
|
}
|
|
|
|
if (loadBtn) {
|
|
loadBtn.addEventListener('click', function () {
|
|
const file = fileInput.files && fileInput.files[0];
|
|
if (!file) {
|
|
// Wenn keine Datei ausgewählt ist, laden wir nichts, behalten aber vorhandenen Text.
|
|
alert('Bitte wählen Sie zuerst eine CSV-Datei aus oder fügen Sie CSV-Text direkt in das Feld ein.');
|
|
return;
|
|
}
|
|
readFileToPreview(file);
|
|
});
|
|
}
|
|
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', function () {
|
|
preview.value = '';
|
|
fileInput.value = '';
|
|
});
|
|
}
|
|
|
|
// Falls der Benutzer direkt die Datei auswählt, füllen wir die Vorschau automatisch.
|
|
if (fileInput) {
|
|
fileInput.addEventListener('change', function () {
|
|
const file = fileInput.files && fileInput.files[0];
|
|
if (file) {
|
|
readFileToPreview(file);
|
|
}
|
|
});
|
|
}
|
|
|
|
// CSV utilities and validation for passwords in preview
|
|
function parseCsvText(text, delimiter) {
|
|
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(l => l !== '');
|
|
if (lines.length === 0) return { headers: [], rows: [] };
|
|
const headers = lines[0].split(delimiter).map(h => h.trim().replace(/^"|"$/g, ''));
|
|
const rows = lines.slice(1).map(l => l.split(delimiter).map(c => c.trim().replace(/^"|"$/g, '')));
|
|
return { headers, rows };
|
|
}
|
|
|
|
function validatePasswordJS(password, sam) {
|
|
const errors = [];
|
|
if (!password || password.length < 7) {
|
|
errors.push('Passwort muss mindestens 7 Zeichen lang sein.');
|
|
}
|
|
let categories = 0;
|
|
if (/[A-Z]/.test(password)) categories++;
|
|
if (/[a-z]/.test(password)) categories++;
|
|
if (/\d/.test(password)) categories++;
|
|
if (/[^A-Za-z0-9]/.test(password)) categories++;
|
|
if (categories < 3) {
|
|
errors.push('Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten.');
|
|
}
|
|
if (sam) {
|
|
const pwLower = password.toLowerCase();
|
|
const samLower = sam.toLowerCase();
|
|
if (pwLower.includes(samLower)) {
|
|
errors.push('Passwort darf den Benutzernamen nicht enthalten.');
|
|
} else {
|
|
const minLen = 4;
|
|
if (samLower.length >= minLen) {
|
|
outer: for (let len = minLen; len <= samLower.length; len++) {
|
|
for (let s = 0; s <= samLower.length - len; s++) {
|
|
const sub = samLower.substr(s, len);
|
|
if (pwLower.includes(sub)) { errors.push('Passwort darf keine größeren Teile des Benutzernamens enthalten.'); break outer; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
function renderCsvPreview(text, delimiter) {
|
|
const parsed = parseCsvText(text, delimiter);
|
|
const headers = parsed.headers;
|
|
const rows = parsed.rows;
|
|
const previewArea = document.getElementById('csvPreviewArea');
|
|
const previewInfo = document.getElementById('csvPreviewInfo');
|
|
const table = document.getElementById('csvPreviewTable');
|
|
table.innerHTML = '';
|
|
if (headers.length === 0) {
|
|
previewArea.style.display = 'none';
|
|
return { invalidCount: 0 };
|
|
}
|
|
|
|
// build header
|
|
const thead = document.createElement('thead');
|
|
const trh = document.createElement('tr');
|
|
headers.forEach(h => { const th = document.createElement('th'); th.textContent = h; trh.appendChild(th); });
|
|
thead.appendChild(trh);
|
|
table.appendChild(thead);
|
|
|
|
// find indexes
|
|
const pwdIdx = headers.findIndex(h => /pass(word)?/i.test(h));
|
|
const samIdx = headers.findIndex(h => /(sam(accountname)?)|samaccountname/i.test(h));
|
|
|
|
const tbody = document.createElement('tbody');
|
|
let invalidCount = 0;
|
|
rows.forEach((rowArr, rowIndex) => {
|
|
const tr = document.createElement('tr');
|
|
rowArr.forEach((cell, colIndex) => {
|
|
const td = document.createElement('td');
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = cell;
|
|
input.className = 'form-control form-control-sm';
|
|
input.addEventListener('input', function () {
|
|
// re-validate this row when edited
|
|
const currentPwd = (pwdIdx >= 0) ? tr.querySelectorAll('input')[pwdIdx].value : '';
|
|
const currentSam = (samIdx >= 0) ? tr.querySelectorAll('input')[samIdx].value : '';
|
|
const errs = validatePasswordJS(currentPwd, currentSam);
|
|
if (errs.length > 0) {
|
|
tr.classList.add('table-danger');
|
|
} else {
|
|
tr.classList.remove('table-danger');
|
|
}
|
|
});
|
|
td.appendChild(input);
|
|
tr.appendChild(td);
|
|
});
|
|
|
|
// validate password for this row if applicable
|
|
if (pwdIdx >= 0) {
|
|
const pwd = rowArr[pwdIdx] || '';
|
|
const sam = (samIdx >= 0) ? (rowArr[samIdx] || '') : '';
|
|
const errs = validatePasswordJS(pwd, sam);
|
|
if (errs.length > 0) {
|
|
tr.classList.add('table-danger');
|
|
invalidCount++;
|
|
}
|
|
}
|
|
|
|
tbody.appendChild(tr);
|
|
});
|
|
table.appendChild(tbody);
|
|
previewArea.style.display = 'block';
|
|
previewInfo.textContent = (invalidCount > 0)
|
|
? `${invalidCount} Zeile(n) haben ungültige Passwörter. Bitte korrigieren Sie diese in der Tabelle bevor Sie die CSV verarbeiten.`
|
|
: 'CSV-Vorschau geladen. Alle Passwörter entsprechen den Anforderungen.';
|
|
return { invalidCount };
|
|
}
|
|
|
|
if (loadBtn) {
|
|
loadBtn.addEventListener('click', function () {
|
|
const file = fileInput.files && fileInput.files[0];
|
|
const delim = document.getElementById('csvdelimiter').value || ',';
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
preview.value = e.target.result || '';
|
|
renderCsvPreview(preview.value, delim);
|
|
};
|
|
reader.readAsText(file, 'utf-8');
|
|
return;
|
|
}
|
|
if (preview.value.trim() === '') {
|
|
alert('Bitte wählen Sie zuerst eine CSV-Datei aus oder fügen Sie CSV-Text direkt in das Feld ein.');
|
|
return;
|
|
}
|
|
renderCsvPreview(preview.value, delim);
|
|
});
|
|
}
|
|
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', function () {
|
|
preview.value = '';
|
|
fileInput.value = '';
|
|
document.getElementById('csvPreviewArea').style.display = 'none';
|
|
});
|
|
}
|
|
|
|
if (fileInput) {
|
|
fileInput.addEventListener('change', function () {
|
|
// auto-load content into preview textarea (but do not auto-validate until user clicks 'In Vorschau laden')
|
|
const file = fileInput.files && fileInput.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
preview.value = e.target.result || '';
|
|
};
|
|
reader.readAsText(file, 'utf-8');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Intercept submit: read file if needed, validate preview passwords, then submit
|
|
function handleCsvSubmit(e) {
|
|
e.preventDefault();
|
|
const delim = document.getElementById('csvdelimiter').value || ',';
|
|
const file = fileInput.files && fileInput.files[0];
|
|
|
|
const proceedWithText = function (text) {
|
|
const res = renderCsvPreview(text, delim);
|
|
if (res.invalidCount > 0) {
|
|
alert('Import abgebrochen: Es gibt ungültige Passwörter in der CSV-Vorschau. Bitte korrigieren Sie diese zuerst.');
|
|
return;
|
|
}
|
|
// ensure preview textarea contains the text we'll submit
|
|
preview.value = text;
|
|
// Remove handler to avoid re-validation loop and submit the form
|
|
form.removeEventListener('submit', handleCsvSubmit);
|
|
form.submit();
|
|
};
|
|
|
|
if (preview.value.trim() === '') {
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function (ev) {
|
|
const text = ev.target.result || '';
|
|
proceedWithText(text);
|
|
};
|
|
reader.onerror = function () {
|
|
alert('Fehler beim Lesen der Datei. Bitte versuchen Sie es erneut.');
|
|
};
|
|
reader.readAsText(file, 'utf-8');
|
|
return;
|
|
}
|
|
alert('Die CSV-Vorschau ist leer. Bitte wählen Sie eine Datei oder fügen Sie CSV-Inhalt ein.');
|
|
return;
|
|
}
|
|
|
|
proceedWithText(preview.value);
|
|
}
|
|
|
|
form.addEventListener('submit', handleCsvSubmit);
|
|
})();
|
|
</script>
|
|
|
|
<?php // Ende der View ?>
|