Compare commits
42 Commits
main
...
snmp_updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9746881492 | ||
|
|
a639cd68e3 | ||
| ee5ce6b5bd | |||
| bbdbb0e187 | |||
| 50e5d82fa3 | |||
| 656093ce01 | |||
| 4127323530 | |||
| 3c2c6d6c6f | |||
| 50d53b4183 | |||
| c194f80f45 | |||
| 434b090b08 | |||
| 300719244e | |||
| 9a8d90a6db | |||
| 21c63b9d6c | |||
| 91d2821034 | |||
| e0cd5591c5 | |||
| e82c2c041a | |||
| f9f9b1f99b | |||
| daba87e48f | |||
| bce1a0c719 | |||
| d013b6cb8a | |||
| bde7b7c7ce | |||
| 191666736b | |||
| 2637715e48 | |||
| bef54a7553 | |||
| bdd79a66cb | |||
| 3eac9a659e | |||
| d1bafa28d9 | |||
| cfcc8f2d0c | |||
| 28be78dceb | |||
| ab296ad0dc | |||
| f68db3ab1e | |||
| 905ce67742 | |||
| 8cd07a73a0 | |||
| 77ac0577e2 | |||
| 7f7cab75e5 | |||
| 6ad293ee4f | |||
| 5acc6d93c9 | |||
| dc75190642 | |||
| b82d16a064 | |||
| 195f19a6f2 | |||
| 6c67d6437e |
36
README.md
36
README.md
@ -65,11 +65,8 @@ Der komplette Ablauf ist im [Gitea-Workflow](https://git.eckertplayground.de/taa
|
||||
| Benutzer und Gruppen über LDAP anzeigen | Jens E (@blaerf), Stefan W (@viperion) |
|
||||
| SNMP Serverstatus abfragen / anzeigen |Thomas G (@tg95) |
|
||||
| PHP-Powershell Anbindung | Marco Z (@Taarly) |
|
||||
| Powershell Script für einzelne Benutzer und CSV Import | Matthias K |
|
||||
| UI/UX anpassen | Yasin B (@Muchentuchen), Alexander M (@Alexander) |
|
||||
| Blackbox Testing | Torsten J (@tojacobs) |
|
||||
|
||||
**Hinweis:** Die Passwortanforderungen (Mindestlänge, Kategorien, keine Teile des Benutzernamens) werden beim Erstellen validiert. Die Validierung ist in `scripts/powershell/create_users_csv.ps1` implementiert und die Mockup-UI (`docs/Mockup/index.html`) zeigt die Anforderungen und prüft sie clientseitig.
|
||||
| Powershell Script für einzelne Benutzer und CSV Import | Alle Fisis |
|
||||
| UI/UX anpassen | Yasin B (@Muchentuchen), Alexander M (@Alexander), Torsten J (@tojacobs) |
|
||||
|
||||
---
|
||||
|
||||
@ -84,35 +81,6 @@ Dieser Bereich muss von allen Entwicklern gelesen werden, bevor am Projekt gearb
|
||||
|
||||
---
|
||||
|
||||
## PowerShell Integration (Benutzererstellung)
|
||||
|
||||
Die Weboberfläche nutzt PowerShell-Skripte, um Active Directory Benutzer anzulegen. Damit dies funktioniert, sind folgende Voraussetzungen erforderlich:
|
||||
|
||||
- Der Webserver läuft auf Windows und PHP kann PowerShell ausführen (`powershell` oder `pwsh`).
|
||||
- Die PowerShell-Module `ActiveDirectory` müssen installiert (RSAT) und verfügbar sein.
|
||||
- Der Benutzer, unter dem der Webserver läuft, muss ausreichende Rechte besitzen, um `New-ADUser` und `Add-ADGroupMember` auszuführen.
|
||||
- Im `config/config.php` kann `powershell.dry_run` auf `true` gesetzt werden, um Tests ohne Änderungen durchzuführen.
|
||||
|
||||
Konfigurationsoptionen (in `config/config.php`):
|
||||
- `powershell.exe`: Name oder Pfad zur PowerShell-Executable (standard `powershell`).
|
||||
- `powershell.script_dir`: Pfad zu den PowerShell-Skripten (standard `scripts/powershell`).
|
||||
- `powershell.execution_policy`: Auszuführende ExecutionPolicy (z. B. `Bypass`).
|
||||
- `powershell.dry_run`: Wenn `true`, werden keine echten AD-Änderungen durchgeführt; das Skript meldet nur, was es tun würde.
|
||||
|
||||
Die grundlegende Funktionalität wurde mit folgenden Komponenten implementiert:
|
||||
- `public/api/create_user.php`: API-Endpoint zur Erstellung eines einzelnen Benutzers.
|
||||
- `public/api/create_users_csv.php`: API-Endpoint zur Erstellung mehrerer Benutzer aus CSV.
|
||||
- `scripts/powershell/create_user.ps1`: PowerShell-Skript zum Erstellen eines einzelnen Benutzers.
|
||||
- `scripts/powershell/create_users_csv.ps1`: PowerShell-Skript zum Erstellen mehrerer Benutzer aus CSV.
|
||||
|
||||
- `scripts/powershell/check_environment.ps1`: Prüft, ob `ActiveDirectory`-Modul vorhanden ist und zeigt die ausführende Identität an.
|
||||
|
||||
API endpoints:
|
||||
- `public/api/powershell_check.php`: Ruft `check_environment.ps1` auf und gibt ein JSON-Objekt mit `actor`, `module_installed`, `can_new_aduser` zurück.
|
||||
|
||||
Bitte testen zuerst mit `powershell.dry_run = true` und prüfen sie die resultierenden Meldungen in UI.
|
||||
|
||||
|
||||
## Mitwirken
|
||||
|
||||
Wer etwas ändern oder erweitern möchte:
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Services\Logging\LoggingService;
|
||||
use App\Services\Logging\LogViewerService;
|
||||
|
||||
/**
|
||||
* Controller für den Log Viewer (read-only).
|
||||
*/
|
||||
class LogViewerController
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $config;
|
||||
|
||||
private LoggingService $logger;
|
||||
private LogViewerService $logViewer;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
$loggingConfig = $config['logging'] ?? [];
|
||||
$this->logger = new LoggingService($loggingConfig);
|
||||
$this->logViewer = new LogViewerService($loggingConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt den Log Viewer an.
|
||||
*
|
||||
* Erwartet optionale GET-Parameter:
|
||||
* - file (Dateiname, z.B. app.log)
|
||||
* - level (DEBUG|INFO|WARNING|ERROR)
|
||||
* - q (Suche)
|
||||
* - lines (Anzahl Zeilen, Default 200)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function show(): array
|
||||
{
|
||||
$files = $this->logViewer->listLogFiles();
|
||||
|
||||
$selectedFile = (string)($_GET['file'] ?? '');
|
||||
if ($selectedFile === '' && isset($files[0]['name'])) {
|
||||
$selectedFile = (string)$files[0]['name'];
|
||||
}
|
||||
|
||||
$level = (string)($_GET['level'] ?? '');
|
||||
$level = strtoupper(trim($level));
|
||||
if ($level === '') {
|
||||
$level = '';
|
||||
}
|
||||
|
||||
$q = (string)($_GET['q'] ?? '');
|
||||
$q = trim($q);
|
||||
|
||||
$lines = (int)($_GET['lines'] ?? 200);
|
||||
if ($lines <= 0) {
|
||||
$lines = 200;
|
||||
}
|
||||
if ($lines > 2000) {
|
||||
$lines = 2000;
|
||||
}
|
||||
|
||||
$error = null;
|
||||
$fileMeta = null;
|
||||
$entries = [];
|
||||
|
||||
try {
|
||||
if ($selectedFile !== '') {
|
||||
$fileMeta = $this->logViewer->getFileMeta($selectedFile);
|
||||
$entries = $this->logViewer->getEntries(
|
||||
$selectedFile,
|
||||
$lines,
|
||||
$level !== '' ? $level : null,
|
||||
$q !== '' ? $q : null
|
||||
);
|
||||
|
||||
if ($fileMeta === null) {
|
||||
$error = 'Die ausgewählte Log-Datei ist nicht verfügbar.';
|
||||
$entries = [];
|
||||
}
|
||||
} else {
|
||||
$error = 'Es wurde keine Log-Datei gefunden.';
|
||||
}
|
||||
} catch (\Throwable $ex) {
|
||||
$this->logger->logException(
|
||||
'LogViewerController: Fehler beim Laden der Logs.',
|
||||
$ex,
|
||||
[
|
||||
'route' => 'logs',
|
||||
'file' => $selectedFile,
|
||||
]
|
||||
);
|
||||
|
||||
$error = 'Technischer Fehler beim Laden der Logs. Details stehen im app.log.';
|
||||
}
|
||||
|
||||
$viewPath = __DIR__ . '/../../public/views/logs.php';
|
||||
|
||||
return [
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
'loginPage' => false,
|
||||
'files' => $files,
|
||||
'selectedFile' => $selectedFile,
|
||||
'fileMeta' => $fileMeta,
|
||||
'entries' => $entries,
|
||||
'filterLevel' => $level,
|
||||
'searchQuery' => $q,
|
||||
'lines' => $lines,
|
||||
'error' => $error,
|
||||
],
|
||||
'pageTitle' => 'Logs',
|
||||
'activeMenu' => 'logs',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -100,49 +100,4 @@ class UserManagementController
|
||||
'activeMenu' => 'users',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt die Seite zum Erstellen von Benutzern (Einzel/CSV).
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function create(): array
|
||||
{
|
||||
$viewPath = __DIR__ . '/../../public/views/createuser.php';
|
||||
|
||||
// Use session flash messages if available
|
||||
$error = null;
|
||||
$success = null;
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
@session_start();
|
||||
}
|
||||
if (isset($_SESSION['flash_error'])) {
|
||||
$error = $_SESSION['flash_error'];
|
||||
unset($_SESSION['flash_error']);
|
||||
}
|
||||
if (isset($_SESSION['flash_success'])) {
|
||||
$success = $_SESSION['flash_success'];
|
||||
unset($_SESSION['flash_success']);
|
||||
}
|
||||
$csvDetails = null;
|
||||
if (isset($_SESSION['csv_details'])) {
|
||||
$csvDetails = $_SESSION['csv_details'];
|
||||
unset($_SESSION['csv_details']);
|
||||
}
|
||||
|
||||
$powershellDryRun = $this->config['powershell']['dry_run'] ?? false;
|
||||
|
||||
return [
|
||||
'view' => $viewPath,
|
||||
'data' => [
|
||||
'error' => $error,
|
||||
'success' => $success,
|
||||
'loginPage' => false,
|
||||
'csvDetails' => $csvDetails,
|
||||
'powershellDryRun' => $powershellDryRun,
|
||||
],
|
||||
'pageTitle' => 'Benutzer erstellen',
|
||||
'activeMenu' => 'createuser',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,339 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Logging;
|
||||
|
||||
/**
|
||||
* LogViewerService
|
||||
*
|
||||
* - Listet Log-Dateien im konfigurierten Logging-Verzeichnis.
|
||||
* - Liest performant die letzten N Zeilen (Tail), ohne komplette Datei zu laden.
|
||||
* - Parst das LoggingService-Format:
|
||||
* "[YYYY-MM-DD HH:MM:SS] LEVEL message {json}"
|
||||
*/
|
||||
class LogViewerService
|
||||
{
|
||||
private string $logDir;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $loggingConfig Teilkonfiguration "logging" aus config.php
|
||||
*/
|
||||
public function __construct(array $loggingConfig)
|
||||
{
|
||||
$baseDir = $loggingConfig['log_dir'] ?? (__DIR__ . '/../../../public/logs');
|
||||
$this->logDir = rtrim((string)$baseDir, DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{name:string, size:int, mtime:int}>
|
||||
*/
|
||||
public function listLogFiles(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (is_dir($this->logDir) === false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$entries = @scandir($this->logDir);
|
||||
if ($entries === false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($entries as $name) {
|
||||
if ($name === '.' || $name === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nur normale Dateien, keine Unterordner.
|
||||
$fullPath = $this->logDir . DIRECTORY_SEPARATOR . $name;
|
||||
if (is_file($fullPath) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safety: nur "logartige" Dateien anzeigen.
|
||||
$lower = strtolower($name);
|
||||
if (
|
||||
str_ends_with($lower, '.log') === false
|
||||
&& str_ends_with($lower, '.txt') === false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$size = @filesize($fullPath);
|
||||
$mtime = @filemtime($fullPath);
|
||||
|
||||
$result[] = [
|
||||
'name' => $name,
|
||||
'size' => is_int($size) ? $size : 0,
|
||||
'mtime' => is_int($mtime) ? $mtime : 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Neueste zuerst
|
||||
usort(
|
||||
$result,
|
||||
static function (array $a, array $b): int {
|
||||
return ($b['mtime'] ?? 0) <=> ($a['mtime'] ?? 0);
|
||||
}
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert Metadaten zur ausgewählten Datei (oder null wenn ungültig).
|
||||
*
|
||||
* @return array{name:string, size:int, mtime:int}|null
|
||||
*/
|
||||
public function getFileMeta(string $fileName): ?array
|
||||
{
|
||||
$path = $this->resolveLogFilePath($fileName);
|
||||
if ($path === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$size = @filesize($path);
|
||||
$mtime = @filemtime($path);
|
||||
|
||||
return [
|
||||
'name' => basename($path),
|
||||
'size' => is_int($size) ? $size : 0,
|
||||
'mtime' => is_int($mtime) ? $mtime : 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param int $maxLines
|
||||
* @param string|null $levelFilter z.B. "ERROR"|"WARNING"|"INFO"|"DEBUG"|null
|
||||
* @param string|null $search Freitextsuche in message/context/raw
|
||||
*
|
||||
* @return array<int, array{
|
||||
* ts:string|null,
|
||||
* level:string|null,
|
||||
* message:string,
|
||||
* context:array<string,mixed>|null,
|
||||
* raw:string
|
||||
* }>
|
||||
*/
|
||||
public function getEntries(string $fileName, int $maxLines = 200, ?string $levelFilter = null, ?string $search = null): array
|
||||
{
|
||||
$path = $this->resolveLogFilePath($fileName);
|
||||
if ($path === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = $this->tailLines($path, $maxLines);
|
||||
|
||||
$entries = [];
|
||||
foreach ($lines as $line) {
|
||||
$parsed = $this->parseLine($line);
|
||||
|
||||
if ($levelFilter !== null && $levelFilter !== '') {
|
||||
$lvl = strtoupper((string)($parsed['level'] ?? ''));
|
||||
if ($lvl !== strtoupper($levelFilter)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($search !== null && $search !== '') {
|
||||
$haystack = $parsed['raw'] . ' ' . $parsed['message'];
|
||||
|
||||
if (is_array($parsed['context'])) {
|
||||
$json = json_encode(
|
||||
$parsed['context'],
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR
|
||||
);
|
||||
if ($json !== false) {
|
||||
$haystack .= ' ' . $json;
|
||||
}
|
||||
}
|
||||
|
||||
if (mb_stripos($haystack, $search) === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$entries[] = $parsed;
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null Vollständiger Pfad oder null, wenn ungültig/Traversal
|
||||
*/
|
||||
private function resolveLogFilePath(string $fileName): ?string
|
||||
{
|
||||
$fileName = trim($fileName);
|
||||
if ($fileName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Keine Pfade erlauben, nur Dateiname
|
||||
$fileName = basename($fileName);
|
||||
|
||||
$candidate = $this->logDir . DIRECTORY_SEPARATOR . $fileName;
|
||||
|
||||
$realDir = realpath($this->logDir);
|
||||
$realFile = realpath($candidate);
|
||||
|
||||
if ($realDir === false || $realFile === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Muss innerhalb des Log-Verzeichnisses liegen
|
||||
$realDirNorm = rtrim($realDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
$realFileNorm = $realFile;
|
||||
|
||||
if (str_starts_with($realFileNorm, $realDirNorm) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_file($realFileNorm) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $realFileNorm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tail-Implementierung: liest die letzten $maxLines Zeilen.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function tailLines(string $filePath, int $maxLines): array
|
||||
{
|
||||
$maxLines = max(1, min(2000, $maxLines));
|
||||
|
||||
$fh = @fopen($filePath, 'rb');
|
||||
if ($fh === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$chunkSize = 8192;
|
||||
$buffer = '';
|
||||
$pos = -1;
|
||||
$linesFound = 0;
|
||||
|
||||
// ans Ende springen
|
||||
@fseek($fh, 0, SEEK_END);
|
||||
$fileSize = (int)@ftell($fh);
|
||||
if ($fileSize <= 0) {
|
||||
@fclose($fh);
|
||||
return [];
|
||||
}
|
||||
|
||||
while ($linesFound <= $maxLines && -$pos < $fileSize) {
|
||||
$readSize = $chunkSize;
|
||||
if (-$pos + $chunkSize > $fileSize) {
|
||||
$readSize = $fileSize - (-$pos);
|
||||
}
|
||||
|
||||
@fseek($fh, $pos - $readSize + 1, SEEK_END);
|
||||
$chunk = (string)@fread($fh, $readSize);
|
||||
|
||||
$buffer = $chunk . $buffer;
|
||||
$linesFound = substr_count($buffer, "\n");
|
||||
|
||||
$pos -= $readSize;
|
||||
}
|
||||
|
||||
@fclose($fh);
|
||||
|
||||
$lines = preg_split("/\r\n|\n|\r/", $buffer);
|
||||
if (is_array($lines) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ggf. letzte leere Zeile raus
|
||||
if (end($lines) === '') {
|
||||
array_pop($lines);
|
||||
}
|
||||
|
||||
// letzte maxLines
|
||||
$lines = array_slice($lines, -$maxLines);
|
||||
|
||||
// Leere Zeilen am Anfang/Ende tolerieren, aber nicht komplett aufblasen
|
||||
$clean = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = (string)$line;
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$clean[] = $line;
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* ts:string|null,
|
||||
* level:string|null,
|
||||
* message:string,
|
||||
* context:array<string,mixed>|null,
|
||||
* raw:string
|
||||
* }
|
||||
*/
|
||||
private function parseLine(string $line): array
|
||||
{
|
||||
$raw = $line;
|
||||
|
||||
$ts = null;
|
||||
$level = null;
|
||||
$message = $line;
|
||||
$context = null;
|
||||
|
||||
// Basis: [timestamp] LEVEL ...
|
||||
if (preg_match('/^\[(?<ts>[0-9\-:\s]{19})]\s+(?<lvl>[A-Za-z]+)\s+(?<rest>.*)$/', $line, $m) === 1) {
|
||||
$ts = (string)$m['ts'];
|
||||
$level = (string)$m['lvl'];
|
||||
$rest = (string)$m['rest'];
|
||||
|
||||
// Versuch: Context ist am Ende ein JSON-Objekt, das mit "{" beginnt und mit "}" endet.
|
||||
$ctxCandidate = null;
|
||||
|
||||
$lastBracePos = strrpos($rest, '{');
|
||||
if ($lastBracePos !== false) {
|
||||
$maybeJson = substr($rest, $lastBracePos);
|
||||
$maybeJson = trim($maybeJson);
|
||||
|
||||
if ($maybeJson !== '' && str_starts_with($maybeJson, '{') && str_ends_with($maybeJson, '}')) {
|
||||
$decoded = json_decode($maybeJson, true);
|
||||
if (is_array($decoded)) {
|
||||
$ctxCandidate = $decoded;
|
||||
$rest = trim(substr($rest, 0, $lastBracePos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$context = $ctxCandidate;
|
||||
$message = $rest;
|
||||
}
|
||||
|
||||
if ($level === null) {
|
||||
$upper = strtoupper($raw);
|
||||
|
||||
if (str_starts_with($upper, 'PHP WARNING') || str_starts_with($upper, 'WARNING')) {
|
||||
$level = 'WARNING';
|
||||
} elseif (str_starts_with($upper, 'PHP NOTICE') || str_starts_with($upper, 'NOTICE')) {
|
||||
$level = 'INFO';
|
||||
} elseif (str_starts_with($upper, 'PHP FATAL') || str_contains($upper, 'FATAL ERROR') || str_starts_with($upper, 'UNCAUGHT')) {
|
||||
$level = 'ERROR';
|
||||
} else {
|
||||
$level = 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ts' => $ts,
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'raw' => $raw,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -60,16 +60,4 @@ return [
|
||||
// Minimale Stufe: debug, info, warning, error
|
||||
'min_level' => 'info',
|
||||
],
|
||||
'powershell' => [
|
||||
// Executable name: 'powershell' on Windows, 'pwsh' for PowerShell core.
|
||||
'exe' => 'powershell',
|
||||
// Script directory where the PS1 scripts live (relative to config dir)
|
||||
'script_dir' => __DIR__ . '/../scripts/powershell',
|
||||
// Execution policy to pass to the PowerShell invocation
|
||||
'execution_policy' => 'Bypass',
|
||||
// Default OU. IIS only has write access there.
|
||||
'default_ou' => 'OU=WebAppUsers,DC=ITFA-PROJ-DOM,DC=local',
|
||||
// For testing; if true, the scripts will run in dry-run mode (no real AD changes)
|
||||
'dry_run' => false,
|
||||
],
|
||||
];
|
||||
|
||||
@ -262,10 +262,6 @@
|
||||
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Password hint and invalid input styles */
|
||||
.password-hint { display:block; margin-top: 6px; font-size: 0.9rem; color: #6c757d; }
|
||||
.invalid { border: 1px solid #c0152f; background-color: rgba(192,21,47,0.04); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -284,7 +280,6 @@
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<small class="text-muted password-hint">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">
|
||||
@ -382,60 +377,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
document.getElementById('adForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const firstname = document.getElementById('firstname').value.trim();
|
||||
const lastname = document.getElementById('lastname').value.trim();
|
||||
const samGuess = (firstname + lastname).replace(/\s+/g, '');
|
||||
const password = document.getElementById('password').value;
|
||||
const pwErrors = validatePasswordJS(password, samGuess);
|
||||
if (pwErrors.length > 0) {
|
||||
alert('Passwort ungültig:\n' + pwErrors.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const formData = {
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
firstname: document.getElementById('firstname').value,
|
||||
lastname: document.getElementById('lastname').value,
|
||||
group: document.getElementById('group').value,
|
||||
password: password,
|
||||
password: document.getElementById('password').value,
|
||||
customFields: {}
|
||||
};
|
||||
|
||||
@ -493,9 +442,6 @@
|
||||
|
||||
// Datenzeilen erstellen (mit editierbaren Inputs)
|
||||
tableBody.innerHTML = '';
|
||||
const pwdHeaderIndex = headers.findIndex(h => /pass(word)?/i.test(h));
|
||||
const samHeaderIndex = headers.findIndex(h => /(sam(accountname)?)|samaccountname/i.test(h));
|
||||
let foundInvalid = false;
|
||||
for (let i = 1; i < csvData.length; i++) {
|
||||
const row = csvData[i];
|
||||
const tr = document.createElement('tr');
|
||||
@ -508,10 +454,6 @@
|
||||
input.dataset.col = index;
|
||||
input.addEventListener('input', (e) => {
|
||||
csvData[i][index] = e.target.value;
|
||||
// live re-validate if this is password or sam column
|
||||
if (index === pwdHeaderIndex || index === samHeaderIndex) {
|
||||
validateCsvPasswords(headers);
|
||||
}
|
||||
});
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
@ -519,53 +461,10 @@
|
||||
tableBody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Validate password column if present
|
||||
function validateCsvPasswords(headersLocal) {
|
||||
const pwdIdx = pwdHeaderIndex;
|
||||
const samIdx = samHeaderIndex;
|
||||
foundInvalid = false;
|
||||
// Clear previous highlights/messages
|
||||
const rows = tableBody.querySelectorAll('tr');
|
||||
rows.forEach((r, idx) => {
|
||||
r.querySelectorAll('input').forEach(inp => inp.classList.remove('invalid'));
|
||||
});
|
||||
|
||||
if (pwdIdx >= 0) {
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
const inputs = rows[r].querySelectorAll('input');
|
||||
const pwdVal = inputs[pwdIdx] ? inputs[pwdIdx].value : '';
|
||||
const samVal = (samIdx >= 0 && inputs[samIdx]) ? inputs[samIdx].value : '';
|
||||
const errs = validatePasswordJS(pwdVal, samVal);
|
||||
if (errs.length > 0) {
|
||||
foundInvalid = true;
|
||||
if (inputs[pwdIdx]) inputs[pwdIdx].classList.add('invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previewInfo = previewDiv.querySelector('.preview-info');
|
||||
if (!previewInfo) return;
|
||||
if (foundInvalid) {
|
||||
previewInfo.innerHTML = '<strong>Hinweis:</strong> Einige Passwörter entsprechen nicht den Anforderungen. Bitte korrigieren Sie diese in der Tabelle bevor Sie importieren.';
|
||||
} else {
|
||||
previewInfo.innerHTML = '<strong>Hinweis:</strong> Sie können die Werte direkt in der Tabelle bearbeiten, bevor Sie importieren.';
|
||||
}
|
||||
}
|
||||
|
||||
// initial validation run
|
||||
validateCsvPasswords(headers);
|
||||
|
||||
previewDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function importCSVData() {
|
||||
// Prevent import if any password entries are invalid (highlighted)
|
||||
const invalids = document.querySelectorAll('#csvTableBody input.invalid');
|
||||
if (invalids.length > 0) {
|
||||
alert('Import abgebrochen: Es gibt ungültige Passwörter in der CSV-Vorschau. Bitte korrigieren Sie diese zuerst.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Importierte CSV-Daten:', csvData);
|
||||
alert(`${csvData.length - 1} Benutzer erfolgreich importiert!\n\nDaten in der Konsole (F12) einsehen.`);
|
||||
cancelCSVPreview();
|
||||
|
||||
@ -204,13 +204,6 @@
|
||||
: mixed </span>
|
||||
</dt>
|
||||
|
||||
<dt class="phpdocumentor-table-of-contents__entry -method -public">
|
||||
<a class="" href="classes/App-Controllers-UserManagementController.html#method_create">create()</a>
|
||||
<span>
|
||||
: array<string, mixed> </span>
|
||||
</dt>
|
||||
<dd>Zeigt die Seite zum Erstellen von Benutzern (Einzel/CSV).</dd>
|
||||
|
||||
<dt class="phpdocumentor-table-of-contents__entry -method -public">
|
||||
<a class="" href="classes/App-Controllers-UserManagementController.html#method_show">show()</a>
|
||||
<span>
|
||||
@ -404,45 +397,6 @@
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
<article
|
||||
class="phpdocumentor-element
|
||||
-method
|
||||
-public
|
||||
"
|
||||
>
|
||||
<h4 class="phpdocumentor-element__name" id="method_create">
|
||||
create()
|
||||
<a href="classes/App-Controllers-UserManagementController.html#method_create" class="headerlink"><i class="fas fa-link"></i></a>
|
||||
|
||||
</h4>
|
||||
<aside class="phpdocumentor-element-found-in">
|
||||
<abbr class="phpdocumentor-element-found-in__file" title="app/Controllers/UserManagementController.php"><a href="files/app-controllers-usermanagementcontroller.html"><abbr title="app/Controllers/UserManagementController.php">UserManagementController.php</abbr></a></abbr>
|
||||
:
|
||||
<span class="phpdocumentor-element-found-in__line">109</span>
|
||||
|
||||
</aside>
|
||||
|
||||
<p class="phpdocumentor-summary">Zeigt die Seite zum Erstellen von Benutzern (Einzel/CSV).</p>
|
||||
|
||||
<code class="phpdocumentor-code phpdocumentor-signature ">
|
||||
<span class="phpdocumentor-signature__visibility">public</span>
|
||||
<span class="phpdocumentor-signature__name">create</span><span>(</span><span>)</span><span> : </span><span class="phpdocumentor-signature__response_type">array<string, mixed></span></code>
|
||||
|
||||
<div class="phpdocumentor-label-line">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
<h5 class="phpdocumentor-return-value__heading">Return values</h5>
|
||||
<span class="phpdocumentor-signature__response_type">array<string, mixed></span>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
<article
|
||||
class="phpdocumentor-element
|
||||
@ -614,7 +568,6 @@
|
||||
<li>
|
||||
<ul class="phpdocumentor-list -clean">
|
||||
<li class=""><a href="classes/App-Controllers-UserManagementController.html#method___construct">__construct()</a></li>
|
||||
<li class=""><a href="classes/App-Controllers-UserManagementController.html#method_create">create()</a></li>
|
||||
<li class=""><a href="classes/App-Controllers-UserManagementController.html#method_show">show()</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@ -80,11 +80,6 @@ Search.appendIndex(
|
||||
"name": "show",
|
||||
"summary": "Zeigt\u0020Benutzer\u002D\u0020und\u0020Gruppenliste\u0020an.",
|
||||
"url": "classes/App-Controllers-UserManagementController.html#method_show"
|
||||
}, {
|
||||
"fqsen": "\\App\\Controllers\\UserManagementController\u003A\u003Acreate\u0028\u0029",
|
||||
"name": "create",
|
||||
"summary": "Zeigt\u0020die\u0020Seite\u0020zum\u0020Erstellen\u0020von\u0020Benutzern\u0020\u0028Einzel\/CSV\u0029.",
|
||||
"url": "classes/App-Controllers-UserManagementController.html#method_create"
|
||||
}, {
|
||||
"fqsen": "\\App\\Controllers\\UserManagementController\u003A\u003A\u0024config",
|
||||
"name": "config",
|
||||
|
||||
@ -123,7 +123,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="phpdocumentor-cell">TODO</td>
|
||||
<td class="phpdocumentor-cell">223</td>
|
||||
<td class="phpdocumentor-cell">218</td>
|
||||
<td class="phpdocumentor-cell">OS dynamisch per SNMP abfragen (OID 1.3.6.1.2.1.1.1.0)
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
|
||||
// Load config
|
||||
$config = require __DIR__ . '/../../config/config.php';
|
||||
|
||||
// Simple login check (same as index.php)
|
||||
$sessionKey = $config['security']['session_key_user'] ?? 'admin_user';
|
||||
if (!isset($_SESSION[$sessionKey])) {
|
||||
header('Location: ../index.php?route=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Only accept POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Basic input validation
|
||||
$sam = trim((string)($_POST['samaccountname'] ?? ''));
|
||||
$display = trim((string)($_POST['displayname'] ?? ''));
|
||||
$mail = trim((string)($_POST['mail'] ?? ''));
|
||||
$pass = (string)($_POST['password'] ?? '');
|
||||
$ou = trim((string)($_POST['ou'] ?? ''));
|
||||
$groups = trim((string)($_POST['groups'] ?? ''));
|
||||
|
||||
if ($sam === '' || $pass === '') {
|
||||
$_SESSION['flash_error'] = 'Anmeldename und Passwort sind erforderlich.';
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Server-side password validation (same rules as CSV script)
|
||||
$passwordHint = "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.";
|
||||
function validate_password_php(string $password, string $sam): array {
|
||||
$errors = [];
|
||||
if ($password === '' || mb_strlen($password) < 7) {
|
||||
$errors[] = 'Passwort muss mindestens 7 Zeichen lang sein.';
|
||||
}
|
||||
$categories = 0;
|
||||
if (preg_match('/[A-Z]/u', $password)) $categories++;
|
||||
if (preg_match('/[a-z]/u', $password)) $categories++;
|
||||
if (preg_match('/\d/', $password)) $categories++;
|
||||
if (preg_match('/[^A-Za-z0-9]/u', $password)) $categories++;
|
||||
if ($categories < 3) {
|
||||
$errors[] = 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten.';
|
||||
}
|
||||
$samLower = mb_strtolower($sam);
|
||||
$pwLower = mb_strtolower($password);
|
||||
if ($samLower !== '' && mb_strpos($pwLower, $samLower) !== false) {
|
||||
$errors[] = 'Passwort darf den Benutzernamen nicht enthalten.';
|
||||
} else {
|
||||
$minLen = 4;
|
||||
if (mb_strlen($samLower) >= $minLen) {
|
||||
$samLen = mb_strlen($samLower);
|
||||
for ($len = $minLen; $len <= $samLen; $len++) {
|
||||
for ($start = 0; $start <= $samLen - $len; $start++) {
|
||||
$sub = mb_substr($samLower, $start, $len);
|
||||
if (mb_strpos($pwLower, $sub) !== false) {
|
||||
$errors[] = 'Passwort darf keine größeren Teile des Benutzernamens enthalten.';
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
$pwErrors = validate_password_php($pass, $sam);
|
||||
if (count($pwErrors) > 0) {
|
||||
$_SESSION['flash_error'] = 'Ungültiges Passwort: ' . implode(' | ', $pwErrors) . "\n\nHinweis: $passwordHint";
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($ou === '') {
|
||||
$defaultOu = (string)($config['powershell']['default_ou'] ?? '');
|
||||
if ($defaultOu !== '') {
|
||||
$ou = $defaultOu;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Build payload
|
||||
$payload = [
|
||||
'samaccountname' => $sam,
|
||||
'displayname' => $display,
|
||||
'mail' => $mail,
|
||||
'password' => $pass,
|
||||
'ou' => $ou,
|
||||
'groups' => $groups,
|
||||
'dry_run' => (bool)($config['powershell']['dry_run'] ?? false),
|
||||
];
|
||||
|
||||
// Write payload to temp file
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'create_user_') . '.json';
|
||||
file_put_contents($tmpFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
// Build PS script path
|
||||
$scriptDir = $config['powershell']['script_dir'] ?? __DIR__ . '/../../scripts/powershell';
|
||||
$script = $scriptDir . DIRECTORY_SEPARATOR . 'create_user.ps1';
|
||||
|
||||
$exe = $config['powershell']['exe'] ?? 'powershell';
|
||||
$executionPolicy = $config['powershell']['execution_policy'] ?? 'Bypass';
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s -NoProfile -NonInteractive -ExecutionPolicy %s -File "%s" -InputFile "%s"',
|
||||
$exe,
|
||||
$executionPolicy,
|
||||
$script,
|
||||
$tmpFile
|
||||
);
|
||||
|
||||
// Execute and capture output and exit code
|
||||
$output = [];
|
||||
$returnVar = null;
|
||||
if (!file_exists($script)) {
|
||||
$_SESSION['flash_error'] = 'PowerShell-Skript nicht gefunden: ' . $script;
|
||||
@unlink($tmpFile);
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Try to locate the PowerShell executable
|
||||
$exePathCheck = shell_exec(sprintf('where %s 2>NUL', escapeshellarg($exe)));
|
||||
if ($exePathCheck === null) {
|
||||
// 'where' returns null when command fails; continue anyways, exec will fail if not found
|
||||
}
|
||||
|
||||
exec($cmd . ' 2>&1', $output, $returnVar);
|
||||
$json = implode("\n", $output);
|
||||
|
||||
// Optional: write raw output into logs for debugging
|
||||
$ts = date('Y-m-d H:i:s');
|
||||
|
||||
$ctx = [
|
||||
'cmd' => $cmd,
|
||||
'return_code' => $returnVar,
|
||||
'output' => $json,
|
||||
];
|
||||
|
||||
$line = sprintf(
|
||||
"[%s] %-7s %s %s%s",
|
||||
$ts,
|
||||
'DEBUG',
|
||||
'PowerShell CMD',
|
||||
json_encode($ctx, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
PHP_EOL
|
||||
);
|
||||
|
||||
@file_put_contents(__DIR__ . '/../logs/create_user_output.log', $line, FILE_APPEND | LOCK_EX);
|
||||
|
||||
@unlink($tmpFile);
|
||||
|
||||
// Try to parse JSON output
|
||||
$result = null;
|
||||
if ($json !== '') {
|
||||
$decoded = json_decode($json, true);
|
||||
if (is_array($decoded)) {
|
||||
$result = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === null) {
|
||||
$_SESSION['flash_error'] = 'Unbekannter Fehler beim Ausführen des PowerShell-Skripts: ' . ($json ?: 'Keine Ausgabe');
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
$_SESSION['flash_success'] = $result['message'] ?? 'Benutzer erfolgreich erstellt.';
|
||||
} else {
|
||||
$_SESSION['flash_error'] = $result['message'] ?? 'Fehler beim Erstellen des Benutzers.';
|
||||
}
|
||||
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
@ -1,128 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
|
||||
// Load config
|
||||
$config = require __DIR__ . '/../../config/config.php';
|
||||
|
||||
// Simple login check (same as index.php)
|
||||
$sessionKey = $config['security']['session_key_user'] ?? 'admin_user';
|
||||
if (!isset($_SESSION[$sessionKey])) {
|
||||
header('Location: ../index.php?route=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
$delimiter = (string)($_POST['csvdelimiter'] ?? ',');
|
||||
$hasHeader = (string)($_POST['hasHeader'] ?? '1');
|
||||
|
||||
// Two sources: uploaded file or csvcontent textarea
|
||||
$csvContent = '';
|
||||
if (!empty($_FILES['csvfile']['tmp_name']) && is_uploaded_file($_FILES['csvfile']['tmp_name'])) {
|
||||
$csvContent = file_get_contents($_FILES['csvfile']['tmp_name']);
|
||||
} else {
|
||||
$csvContent = (string)($_POST['csvcontent'] ?? '');
|
||||
}
|
||||
|
||||
if (trim($csvContent) === '') {
|
||||
$_SESSION['flash_error'] = 'CSV ist leer. Bitte Datei auswählen oder Inhalt einfügen.';
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Write CSV to temp file
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'create_users_') . '.csv';
|
||||
file_put_contents($tmpFile, $csvContent);
|
||||
|
||||
// Build payload with options
|
||||
$payload = [
|
||||
'input_file' => $tmpFile,
|
||||
'delimiter' => $delimiter,
|
||||
'has_header' => (bool)((int)$hasHeader),
|
||||
'dry_run' => (bool)($config['powershell']['dry_run'] ?? false),
|
||||
'default_ou' => (string)($config['powershell']['default_ou'] ?? ''),
|
||||
];
|
||||
|
||||
// Save options as JSON as the input to the PS script
|
||||
$metaFile = tempnam(sys_get_temp_dir(), 'create_users_meta_') . '.json';
|
||||
file_put_contents($metaFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$scriptDir = $config['powershell']['script_dir'] ?? __DIR__ . '/../../scripts/powershell';
|
||||
$script = $scriptDir . DIRECTORY_SEPARATOR . 'create_users_csv.ps1';
|
||||
$exe = $config['powershell']['exe'] ?? 'powershell';
|
||||
$executionPolicy = $config['powershell']['execution_policy'] ?? 'Bypass';
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s -NoProfile -NonInteractive -ExecutionPolicy %s -File "%s" -InputFile "%s"',
|
||||
$exe,
|
||||
$executionPolicy,
|
||||
$script,
|
||||
$metaFile
|
||||
);
|
||||
|
||||
$output = [];
|
||||
$returnVar = null;
|
||||
if (!file_exists($script)) {
|
||||
$_SESSION['flash_error'] = 'PowerShell-Skript nicht gefunden: ' . $script;
|
||||
@unlink($tmpFile);
|
||||
@unlink($metaFile);
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
exec($cmd . ' 2>&1', $output, $returnVar);
|
||||
$json = implode("\n", $output);
|
||||
|
||||
@unlink($tmpFile);
|
||||
@unlink($metaFile);
|
||||
|
||||
// Optional: log the CSV script command and raw output to help debugging
|
||||
$ts = date('Y-m-d H:i:s');
|
||||
|
||||
$ctx = [
|
||||
'cmd' => $cmd,
|
||||
'return_code' => $returnVar,
|
||||
'output' => $json,
|
||||
];
|
||||
|
||||
$line = sprintf(
|
||||
"[%s] %-7s %s %s%s",
|
||||
$ts,
|
||||
'DEBUG',
|
||||
'PowerShell CMD',
|
||||
json_encode($ctx, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
PHP_EOL
|
||||
);
|
||||
|
||||
@file_put_contents(__DIR__ . '/../logs/create_users_csv_output.log', $line, FILE_APPEND | LOCK_EX);
|
||||
|
||||
$result = null;
|
||||
if ($json !== '') {
|
||||
$decoded = json_decode($json, true);
|
||||
if (is_array($decoded)) {
|
||||
$result = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === null) {
|
||||
$_SESSION['flash_error'] = 'Unbekannter Fehler beim Ausführen des PowerShell-Skripts: ' . ($json ?: 'Keine Ausgabe');
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
$_SESSION['flash_success'] = $result['message'] ?? 'CSV verarbeitet.';
|
||||
if (!empty($result['details'])) {
|
||||
$_SESSION['csv_details'] = $result['details'];
|
||||
}
|
||||
} else {
|
||||
$_SESSION['flash_error'] = $result['message'] ?? 'Fehler beim Verarbeiten der CSV.';
|
||||
}
|
||||
|
||||
header('Location: ../index.php?route=createuser');
|
||||
exit;
|
||||
@ -1,45 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
|
||||
// Load config
|
||||
$config = require __DIR__ . '/../../config/config.php';
|
||||
|
||||
// Simple login check (same as index.php)
|
||||
$sessionKey = $config['security']['session_key_user'] ?? 'admin_user';
|
||||
if (!isset($_SESSION[$sessionKey])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Not authenticated']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$scriptDir = $config['powershell']['script_dir'] ?? __DIR__ . '/../../scripts/powershell';
|
||||
$script = $scriptDir . DIRECTORY_SEPARATOR . 'check_environment.ps1';
|
||||
$exe = $config['powershell']['exe'] ?? 'powershell';
|
||||
$executionPolicy = $config['powershell']['execution_policy'] ?? 'Bypass';
|
||||
|
||||
if (!file_exists($script)) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Script not found: ' . $script]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$cmd = sprintf('%s -NoProfile -NonInteractive -ExecutionPolicy %s -File "%s"', $exe, $executionPolicy, $script);
|
||||
|
||||
$output = [];
|
||||
$returnVar = null;
|
||||
exec($cmd . ' 2>&1', $output, $returnVar);
|
||||
$json = implode("\n", $output);
|
||||
|
||||
// Attempt to parse JSON
|
||||
$decoded = json_decode($json, true);
|
||||
if ($decoded === null) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid JSON output', 'raw' => $json]);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($decoded);
|
||||
exit;
|
||||
@ -11,27 +11,27 @@
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root {
|
||||
--blue: #3b82f6;
|
||||
--indigo: #6366f1;
|
||||
--purple: #8b5cf6;
|
||||
--pink: #ec4899;
|
||||
--red: #ef4444;
|
||||
--orange: #f97316;
|
||||
--yellow: #f59e0b;
|
||||
--green: #10b981;
|
||||
--teal: #14b8a6;
|
||||
--cyan: #06b6d4;
|
||||
--blue: #4e73df;
|
||||
--indigo: #6610f2;
|
||||
--purple: #6f42c1;
|
||||
--pink: #e83e8c;
|
||||
--red: #e74a3b;
|
||||
--orange: #fd7e14;
|
||||
--yellow: #f6c23e;
|
||||
--green: #1cc88a;
|
||||
--teal: #20c9a6;
|
||||
--cyan: #36b9cc;
|
||||
--white: #fff;
|
||||
--gray: #9ca3af;
|
||||
--gray-dark: #374151;
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
--success: #10b981;
|
||||
--info: #06b6d4;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--light: #1f2937;
|
||||
--dark: #111827;
|
||||
--gray: #858796;
|
||||
--gray-dark: #5a5c69;
|
||||
--primary: #4e73df;
|
||||
--secondary: #858796;
|
||||
--success: #1cc88a;
|
||||
--info: #36b9cc;
|
||||
--warning: #f6c23e;
|
||||
--danger: #e74a3b;
|
||||
--light: #f8f9fc;
|
||||
--dark: #5a5c69;
|
||||
--breakpoint-xs: 0;
|
||||
--breakpoint-sm: 576px;
|
||||
--breakpoint-md: 768px;
|
||||
@ -64,9 +64,9 @@ body {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #ffffff;
|
||||
color: #858796;
|
||||
text-align: left;
|
||||
background-color: #0f172a;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus:not(:focus-visible) {
|
||||
@ -159,13 +159,13 @@ sup {
|
||||
}
|
||||
|
||||
a {
|
||||
color: #60a5fa;
|
||||
color: #4e73df;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #3b82f6;
|
||||
color: #224abe;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@ -364,7 +364,6 @@ h1, h2, h3, h4, h5, h6,
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
h1, .h1 {
|
||||
@ -1486,23 +1485,23 @@ pre code {
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: #ffffff;
|
||||
color: #858796;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.75rem;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #334155;
|
||||
border-top: 1px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid #334155;
|
||||
border-bottom: 2px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.table tbody + tbody {
|
||||
border-top: 2px solid #334155;
|
||||
border-top: 2px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.table-sm th,
|
||||
@ -1511,12 +1510,12 @@ pre code {
|
||||
}
|
||||
|
||||
.table-bordered {
|
||||
border: 1px solid #334155;
|
||||
border: 1px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.table-bordered th,
|
||||
.table-bordered td {
|
||||
border: 1px solid #334155;
|
||||
border: 1px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.table-bordered thead th,
|
||||
@ -1532,12 +1531,12 @@ pre code {
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #858796;
|
||||
background-color: rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.table-primary,
|
||||
@ -1738,9 +1737,9 @@ pre code {
|
||||
}
|
||||
|
||||
.table .thead-light th {
|
||||
color: #e2e8f0;
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #6e707e;
|
||||
background-color: #eaecf4;
|
||||
border-color: #e3e6f0;
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
@ -1834,10 +1833,10 @@ pre code {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #ffffff;
|
||||
background-color: #1e293b;
|
||||
color: #6e707e;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #334155;
|
||||
border: 1px solid #d1d3e2;
|
||||
border-radius: 0.35rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
@ -1859,15 +1858,15 @@ pre code {
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: #ffffff;
|
||||
background-color: #1e293b;
|
||||
border-color: #3b82f6;
|
||||
color: #6e707e;
|
||||
background-color: #fff;
|
||||
border-color: #bac8f3;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
||||
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
|
||||
}
|
||||
|
||||
.form-control::-webkit-input-placeholder {
|
||||
color: #64748b;
|
||||
color: #858796;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -3025,12 +3024,12 @@ input[type="button"].btn-block {
|
||||
padding: 0.5rem 0;
|
||||
margin: 0.125rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: #f8fafc;
|
||||
color: #858796;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background-color: #1e293b;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #334155;
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.35rem;
|
||||
}
|
||||
|
||||
@ -3183,7 +3182,7 @@ input[type="button"].btn-block {
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #334155;
|
||||
border-top: 1px solid #eaecf4;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@ -3192,7 +3191,7 @@ input[type="button"].btn-block {
|
||||
padding: 0.25rem 1.5rem;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: #e2e8f0;
|
||||
color: #3a3b45;
|
||||
text-align: inherit;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
@ -3200,19 +3199,19 @@ input[type="button"].btn-block {
|
||||
}
|
||||
|
||||
.dropdown-item:hover, .dropdown-item:focus {
|
||||
color: #ffffff;
|
||||
color: #2e2f37;
|
||||
text-decoration: none;
|
||||
background-color: #334155;
|
||||
background-color: #eaecf4;
|
||||
}
|
||||
|
||||
.dropdown-item.active, .dropdown-item:active {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #3b82f6;
|
||||
background-color: #4e73df;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled, .dropdown-item:disabled {
|
||||
color: #94a3b8;
|
||||
color: #b7b9cc;
|
||||
pointer-events: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
@ -4490,9 +4489,9 @@ input[type="button"].btn-block {
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: #1e293b;
|
||||
background-color: #fff;
|
||||
background-clip: border-box;
|
||||
border: 1px solid #334155;
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.35rem;
|
||||
}
|
||||
|
||||
@ -4553,8 +4552,8 @@ input[type="button"].btn-block {
|
||||
.card-header {
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-bottom: 0;
|
||||
background-color: #0f172a;
|
||||
border-bottom: 1px solid #334155;
|
||||
background-color: #f8f9fc;
|
||||
border-bottom: 1px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.card-header:first-child {
|
||||
@ -4563,8 +4562,8 @@ input[type="button"].btn-block {
|
||||
|
||||
.card-footer {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background-color: #0f172a;
|
||||
border-top: 1px solid #334155;
|
||||
background-color: #f8f9fc;
|
||||
border-top: 1px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.card-footer:last-child {
|
||||
@ -6492,7 +6491,7 @@ button.bg-dark:focus {
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #1e293b !important;
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
@ -6500,15 +6499,15 @@ button.bg-dark:focus {
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid #334155 !important;
|
||||
border: 1px solid #e3e6f0 !important;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid #334155 !important;
|
||||
border-top: 1px solid #e3e6f0 !important;
|
||||
}
|
||||
|
||||
.border-right {
|
||||
border-right: 1px solid #334155 !important;
|
||||
border-right: 1px solid #e3e6f0 !important;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
@ -9661,7 +9660,7 @@ a.text-dark:hover, a.text-dark:focus {
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #cbd5e1 !important;
|
||||
color: #858796 !important;
|
||||
}
|
||||
|
||||
.text-black-50 {
|
||||
@ -9798,7 +9797,7 @@ a:focus {
|
||||
}
|
||||
|
||||
#wrapper #content-wrapper {
|
||||
background-color: #0f172a;
|
||||
background-color: #f8f9fc;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -9901,8 +9900,8 @@ a:focus {
|
||||
}
|
||||
|
||||
.bg-gradient-primary {
|
||||
background-color: #1e3a8a;
|
||||
background-image: linear-gradient(180deg, #1e40af 0%, #1e3a8a 50%, #172554 100%);
|
||||
background-color: #4e73df;
|
||||
background-image: linear-gradient(180deg, #4e73df 10%, #224abe 100%);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
@ -9997,39 +9996,39 @@ a:focus {
|
||||
}
|
||||
|
||||
.text-gray-100 {
|
||||
color: #ffffff !important;
|
||||
color: #f8f9fc !important;
|
||||
}
|
||||
|
||||
.text-gray-200 {
|
||||
color: #f8fafc !important;
|
||||
color: #eaecf4 !important;
|
||||
}
|
||||
|
||||
.text-gray-300 {
|
||||
color: #f1f5f9 !important;
|
||||
color: #dddfeb !important;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #e2e8f0 !important;
|
||||
color: #d1d3e2 !important;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #cbd5e1 !important;
|
||||
color: #b7b9cc !important;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #cbd5e1 !important;
|
||||
color: #858796 !important;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
color: #e2e8f0 !important;
|
||||
color: #6e707e !important;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #f1f5f9 !important;
|
||||
color: #5a5c69 !important;
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
color: #ffffff !important;
|
||||
color: #3a3b45 !important;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
@ -10170,8 +10169,6 @@ a:focus {
|
||||
|
||||
.topbar {
|
||||
height: 4.375rem;
|
||||
background-color: #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.topbar #sidebarToggleTop {
|
||||
@ -10391,7 +10388,7 @@ a:focus {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 0.5rem;
|
||||
display: block;
|
||||
color: #cbd5e1;
|
||||
color: #3a3b45;
|
||||
text-decoration: none;
|
||||
border-radius: 0.35rem;
|
||||
white-space: nowrap;
|
||||
@ -10399,17 +10396,17 @@ a:focus {
|
||||
|
||||
.sidebar .nav-item .collapse .collapse-inner .collapse-item:hover,
|
||||
.sidebar .nav-item .collapsing .collapse-inner .collapse-item:hover {
|
||||
background-color: #334155;
|
||||
background-color: #eaecf4;
|
||||
}
|
||||
|
||||
.sidebar .nav-item .collapse .collapse-inner .collapse-item:active,
|
||||
.sidebar .nav-item .collapsing .collapse-inner .collapse-item:active {
|
||||
background-color: #475569;
|
||||
background-color: #dddfeb;
|
||||
}
|
||||
|
||||
.sidebar .nav-item .collapse .collapse-inner .collapse-item.active,
|
||||
.sidebar .nav-item .collapsing .collapse-inner .collapse-item.active {
|
||||
color: #3b82f6;
|
||||
color: #4e73df;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -11272,13 +11269,11 @@ form.user .btn-user {
|
||||
footer.sticky-footer {
|
||||
padding: 2rem 0;
|
||||
flex-shrink: 0;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
footer.sticky-footer .copyright {
|
||||
line-height: 1;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
body.sidebar-toggled footer.sticky-footer {
|
||||
|
||||
@ -74,8 +74,6 @@ use App\Controllers\AuthController;
|
||||
use App\Controllers\DashboardController;
|
||||
use App\Controllers\UserManagementController;
|
||||
use App\Services\Logging\LoggingService;
|
||||
use App\Controllers\LogViewerController;
|
||||
|
||||
|
||||
// Globalen Logger initialisieren, damit auch Fehler außerhalb der Controller
|
||||
// (z. B. in index.php selbst) sauber protokolliert werden.
|
||||
@ -201,8 +199,6 @@ function handleResult(?array $result): void
|
||||
$authController = new AuthController($config);
|
||||
$dashboardController = new DashboardController($config);
|
||||
$userManagementController = new UserManagementController($config);
|
||||
$logViewerController = new LogViewerController($config);
|
||||
|
||||
|
||||
// Route aus dem Query-Parameter lesen. Standardroute ist "login",
|
||||
// sodass nicht angemeldete Benutzer automatisch auf die Login-Seite geführt werden.
|
||||
@ -244,18 +240,6 @@ switch ($route) {
|
||||
handleResult($result);
|
||||
break;
|
||||
|
||||
case 'createuser':
|
||||
requireLogin($config);
|
||||
$result = $userManagementController->create();
|
||||
handleResult($result);
|
||||
break;
|
||||
|
||||
case 'logs':
|
||||
requireLogin($config);
|
||||
$result = $logViewerController->show();
|
||||
handleResult($result);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo 'Route nicht gefunden.';
|
||||
|
||||
@ -1,496 +0,0 @@
|
||||
<?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 { flex: 0 0 150px; width: 150px; display:inline-flex; justify-content:center; align-items:center; box-sizing:border-box; white-space:nowrap; }
|
||||
.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 ?>
|
||||
@ -12,22 +12,6 @@ declare(strict_types=1);
|
||||
/** @var array<string, mixed> $serverStatus */
|
||||
?>
|
||||
|
||||
<style>
|
||||
/* Custom Farben für Dashboard-Karten */
|
||||
.border-left-purple {
|
||||
border-left: 0.25rem solid #6f42c1 !important;
|
||||
}
|
||||
.text-purple {
|
||||
color: #6f42c1 !important;
|
||||
}
|
||||
.border-left-amber {
|
||||
border-left: 0.25rem solid #ff9800 !important;
|
||||
}
|
||||
.text-amber {
|
||||
color: #ff9800 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Content Row -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Server-Dashboard</h1>
|
||||
@ -122,11 +106,11 @@ declare(strict_types=1);
|
||||
<div class="row">
|
||||
<!-- Disk C: / Root -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-purple shadow h-100 py-2">
|
||||
<div class="card border-left-secondary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-purple text-uppercase mb-1">
|
||||
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
|
||||
Datenträger C: / Root
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
@ -143,11 +127,11 @@ declare(strict_types=1);
|
||||
|
||||
<!-- Uptime -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-amber shadow h-100 py-2">
|
||||
<div class="card border-left-dark shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-amber text-uppercase mb-1">
|
||||
<div class="text-xs font-weight-bold text-dark text-uppercase mb-1">
|
||||
System Uptime
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
|
||||
@ -1,165 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Log Viewer (Ansicht).
|
||||
*
|
||||
* Erwartete Variablen:
|
||||
* @var array<int, array{name:string, size:int, mtime:int}> $files
|
||||
* @var string $selectedFile
|
||||
* @var array{name:string, size:int, mtime:int}|null $fileMeta
|
||||
* @var array<int, array{ts:string|null, level:string|null, message:string, context:array<string,mixed>|null, raw:string}> $entries
|
||||
* @var string $filterLevel
|
||||
* @var string $searchQuery
|
||||
* @var int $lines
|
||||
* @var string|null $error
|
||||
*/
|
||||
?>
|
||||
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Log Viewer</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; ?>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Filter</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" action="../index.php">
|
||||
<input type="hidden" name="route" value="logs">
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label for="file">Log-Datei</label>
|
||||
<select class="form-control" id="file" name="file">
|
||||
<?php foreach (($files ?? []) as $f) : ?>
|
||||
<?php $name = (string)($f['name'] ?? ''); ?>
|
||||
<option value="<?php echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>"
|
||||
<?php echo ($name === (string)$selectedFile) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="level">Level</label>
|
||||
<select class="form-control" id="level" name="level">
|
||||
<?php
|
||||
$levels = ['', 'ERROR', 'WARNING', 'INFO', 'DEBUG'];
|
||||
foreach ($levels as $lvl) :
|
||||
$label = ($lvl === '') ? 'Alle' : $lvl;
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars($lvl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>"
|
||||
<?php echo ((string)$filterLevel === (string)$lvl) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="q">Suche</label>
|
||||
<input type="text" class="form-control" id="q" name="q"
|
||||
value="<?php echo htmlspecialchars((string)$searchQuery, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>"
|
||||
placeholder="Text in message/context">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="lines">Zeilen</label>
|
||||
<input type="number" class="form-control" id="lines" name="lines"
|
||||
value="<?php echo (int)$lines; ?>" min="1" max="2000">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-1 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
Anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (is_array($fileMeta)) : ?>
|
||||
<div class="small text-gray-600">
|
||||
Datei: <strong><?php echo htmlspecialchars((string)$fileMeta['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></strong>
|
||||
· Größe: <?php echo number_format((int)$fileMeta['size'], 0, ',', '.'); ?> Bytes
|
||||
· Stand: <?php echo ($fileMeta['mtime'] ?? 0) > 0 ? date('d.m.Y H:i:s', (int)$fileMeta['mtime']) : 'n/a'; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Einträge (neueste unten)</h6>
|
||||
<a class="btn btn-sm btn-outline-secondary"
|
||||
href="../index.php?route=logs&file=<?php echo urlencode((string)$selectedFile); ?>&level=<?php echo urlencode((string)$filterLevel); ?>&q=<?php echo urlencode((string)$searchQuery); ?>&lines=<?php echo (int)$lines; ?>">
|
||||
Refresh
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (empty($entries)) : ?>
|
||||
<div class="text-gray-600">Keine Einträge gefunden.</div>
|
||||
<?php else : ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 170px;">Zeit</th>
|
||||
<th style="width: 110px;">Level</th>
|
||||
<th>Message</th>
|
||||
<th style="width: 35%;">Context</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($entries as $e) : ?>
|
||||
<?php
|
||||
$lvl = strtoupper((string)($e['level'] ?? ''));
|
||||
$badge = 'badge-secondary';
|
||||
if ($lvl === 'ERROR') { $badge = 'badge-danger'; }
|
||||
if ($lvl === 'WARNING') { $badge = 'badge-warning'; }
|
||||
if ($lvl === 'INFO') { $badge = 'badge-info'; }
|
||||
if ($lvl === 'DEBUG') { $badge = 'badge-light'; }
|
||||
|
||||
$ctx = $e['context'] ?? null;
|
||||
$ctxPretty = '';
|
||||
if (is_array($ctx)) {
|
||||
$ctxPretty = (string)json_encode($ctx, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars((string)($e['ts'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
|
||||
<td>
|
||||
<span class="badge <?php echo $badge; ?>">
|
||||
<?php echo htmlspecialchars($lvl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars((string)($e['message'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?></td>
|
||||
<td>
|
||||
<?php if ($ctxPretty !== '') : ?>
|
||||
<pre class="mb-0 text-white" style="white-space: pre-wrap;">
|
||||
<?php echo htmlspecialchars($ctxPretty, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
|
||||
</pre>
|
||||
<?php else : ?>
|
||||
<span class="text-gray-600">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="small text-gray-600">
|
||||
Hinweis: Dieser Viewer lädt bewusst nur die letzten Zeilen (Tail), damit große Logfiles die Oberfläche nicht killen.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@ -19,12 +19,12 @@
|
||||
<title>AD Admin Tool<?= isset($pageTitle) ? ' – ' . htmlspecialchars($pageTitle, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : '' ?></title>
|
||||
|
||||
<!-- Custom fonts for this template-->
|
||||
<link href="/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="../../vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
|
||||
<!-- Google Fonts oder lokal, je nach Setup -->
|
||||
<link href="/css/sb-admin-2.css" rel="stylesheet">
|
||||
<link href="../../css/sb-admin-2.min.css" rel="stylesheet">
|
||||
|
||||
<!-- DataTables CSS (falls benötigt) -->
|
||||
<link href="/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="../../vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
@ -21,23 +21,23 @@
|
||||
</a>
|
||||
|
||||
<!-- Bootstrap core JavaScript-->
|
||||
<script src="/vendor/jquery/jquery.min.js"></script>
|
||||
<script src="/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="../../vendor/jquery/jquery.min.js"></script>
|
||||
<script src="../../vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Core plugin JavaScript-->
|
||||
<script src="/vendor/jquery-easing/jquery.easing.min.js"></script>
|
||||
<script src="../../vendor/jquery-easing/jquery.easing.min.js"></script>
|
||||
|
||||
<!-- Custom scripts for all pages-->
|
||||
<script src="/js/sb-admin-2.min.js"></script>
|
||||
<script src="../../js/sb-admin-2.min.js"></script>
|
||||
|
||||
<!-- Page level plugins -->
|
||||
<script src="/vendor/chart.js/Chart.min.js"></script>
|
||||
<script src="/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||
<script src="/vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||
<script src="../../vendor/chart.js/Chart.min.js"></script>
|
||||
<script src="../../vendor/datatables/jquery.dataTables.min.js"></script>
|
||||
<script src="../../vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||
|
||||
<!-- Page level custom scripts -->
|
||||
<script src="/js/demo/datatables-demo.js"></script>
|
||||
<script src="/js/demo/chart-area-demo.js"></script>
|
||||
<script src="/js/demo/chart-pie-demo.js"></script>
|
||||
<script src="../../js/demo/datatables-demo.js"></script>
|
||||
<script src="../../js/demo/chart-area-demo.js"></script>
|
||||
<script src="../../js/demo/chart-pie-demo.js"></script>
|
||||
|
||||
|
||||
|
||||
@ -49,24 +49,6 @@
|
||||
<span>Benutzer & Gruppen</span></a>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Benutzer erstellen -->
|
||||
<li class="nav-item<?= (isset($activeMenu) && $activeMenu === 'createuser') ? ' active' : '' ?>">
|
||||
<a class="nav-link" href="../../index.php?route=createuser">
|
||||
<i class="fas fa-fw fa-user-plus"></i>
|
||||
<span>Benutzer erstellen</span></a>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider d-none d-md-block">
|
||||
|
||||
<!-- Nav Item - Logs -->
|
||||
<li class="nav-item<?= (isset($activeMenu) && $activeMenu === 'logs') ? ' active' : '' ?>">
|
||||
<a class="nav-link" href="../../index.php?route=logs">
|
||||
<i class="fas fa-fw fa-file-alt"></i>
|
||||
<span>Logs</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider d-none d-md-block">
|
||||
|
||||
|
||||
@ -25,10 +25,6 @@ declare(strict_types=1);
|
||||
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Benutzer & Gruppen</h1>
|
||||
<a href="index.php?route=createuser" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
|
||||
<i class="fas fa-user-plus fa-sm text-white-50"></i>
|
||||
Benutzer erstellen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($error !== null): ?>
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
This directory contains PowerShell scripts used by the PHP AdminTool for Active Directory user creation.
|
||||
|
||||
Usage (single user):
|
||||
1. Create a JSON payload file (for example `payload.json`) with contents:
|
||||
```
|
||||
{
|
||||
"samaccountname": "testuser",
|
||||
"displayname": "Test User",
|
||||
"mail": "testuser@example.local",
|
||||
"password": "P@ssw0rd1234",
|
||||
"ou": "OU=Users,DC=example,DC=local",
|
||||
"groups": "Users,IT-Staff",
|
||||
"dry_run": true
|
||||
}
|
||||
```
|
||||
2. Run the script from PowerShell as a user with permission to create AD users (or use `dry_run` true to test):
|
||||
|
||||
```
|
||||
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File .\create_user.ps1 -InputFile C:\temp\payload.json
|
||||
```
|
||||
|
||||
Usage (CSV):
|
||||
1. Create a CSV file with header `samaccountname,displayname,mail,password,ou,groups` (or no header and set `has_header: false` in meta JSON).
|
||||
2. Create a meta JSON file containing the CSV path and options:
|
||||
```
|
||||
{
|
||||
"input_file": "C:\temp\users.csv",
|
||||
"delimiter": ",",
|
||||
"has_header": true,
|
||||
"dry_run": true
|
||||
}
|
||||
```
|
||||
3. Run the CSV script:
|
||||
```
|
||||
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File .\create_users_csv.ps1 -InputFile C:\temp\meta.json
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Ensure the `ActiveDirectory` PowerShell module is installed on the host system (RSAT).
|
||||
- Test with `dry_run` set to `true` first to verify results without modifying AD.
|
||||
- The scripts return a compact JSON object on stdout which the PHP backend expects.
|
||||
- Run the webserver (IIS) as a user that has sufficient rights to run the `New-ADUser` and `Add-ADGroupMember` commands when `dry_run` is disabled.
|
||||
@ -1,17 +0,0 @@
|
||||
# Returns JSON with information about the environment and AD module availability
|
||||
Try {
|
||||
$actor = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
} Catch {
|
||||
$actor = $null
|
||||
}
|
||||
|
||||
# Does the ActiveDirectory module exist?
|
||||
$module = Get-Module -ListAvailable -Name ActiveDirectory -ErrorAction SilentlyContinue
|
||||
$hasModule = $module -ne $null
|
||||
|
||||
# Is New-ADUser available?
|
||||
$canNewAdUser = (Get-Command New-ADUser -ErrorAction SilentlyContinue) -ne $null
|
||||
|
||||
$output = @{ success = $true; actor = $actor; module_installed = $hasModule; can_new_aduser = $canNewAdUser }
|
||||
Write-Output ($output | ConvertTo-Json -Compress)
|
||||
exit 0
|
||||
@ -1,153 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$InputFile
|
||||
)
|
||||
|
||||
# Read input JSON
|
||||
try {
|
||||
$json = Get-Content -Raw -Path $InputFile -ErrorAction Stop
|
||||
$payload = $json | ConvertFrom-Json
|
||||
} catch {
|
||||
$err = $_.Exception.Message
|
||||
Write-Output (@{ success = $false; message = "Failed to read/parse input JSON: $err" } | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Default result
|
||||
$actor = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$result = @{ success = $false; message = "Unspecified error"; actor = $actor }
|
||||
|
||||
# Validate
|
||||
if (-not $payload.samaccountname -or -not $payload.password) {
|
||||
$result.message = "Required fields: samaccountname and password"
|
||||
Write-Output ($result | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Convert to strings
|
||||
$sam = [string]$payload.samaccountname
|
||||
$display = [string]($payload.displayname)
|
||||
$mail = [string]($payload.mail)
|
||||
$pass = [string]$payload.password
|
||||
$ou = [string]($payload.ou)
|
||||
$groups = [string]($payload.groups)
|
||||
$dryRun = [bool]($payload.dry_run -as [bool])
|
||||
# Password hint and validation helper (German)
|
||||
$passwordHint = @"
|
||||
Das sollten die Anforderungen an das Passwort sein:
|
||||
|
||||
- mindestens 7 Zeichen
|
||||
- darf den Benutzer-/Accountnamen nicht enthalten (bzw. keine zu großen Teile davon)
|
||||
- muss Zeichen aus mindestens 3 von 4 Kategorien enthalten:
|
||||
1) Großbuchstaben (A–Z)
|
||||
2) Kleinbuchstaben (a–z)
|
||||
3) Ziffern (0–9)
|
||||
4) Sonderzeichen (alles, was kein Buchstabe/Zahl ist, z. B. ! ? # _ - . , usw.)
|
||||
"@
|
||||
|
||||
function Test-PasswordRequirements {
|
||||
param(
|
||||
[string]$Password,
|
||||
[string]$SamAccountName
|
||||
)
|
||||
|
||||
$errors = @()
|
||||
|
||||
if ([string]::IsNullOrEmpty($Password) -or $Password.Length -lt 7) {
|
||||
$errors += 'Passwort muss mindestens 7 Zeichen lang sein.'
|
||||
}
|
||||
|
||||
$categories = 0
|
||||
if ($Password -match '[A-Z]') { $categories++ }
|
||||
if ($Password -match '[a-z]') { $categories++ }
|
||||
if ($Password -match '\d') { $categories++ }
|
||||
if ($Password -match '[^A-Za-z0-9]') { $categories++ }
|
||||
if ($categories -lt 3) {
|
||||
$errors += 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten (Groß, Klein, Ziffern, Sonderzeichen).'
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($SamAccountName)) {
|
||||
$pwLower = $Password.ToLowerInvariant()
|
||||
$samLower = $SamAccountName.ToLowerInvariant()
|
||||
|
||||
if ($pwLower -like "*${samLower}*") {
|
||||
$errors += 'Passwort darf den Benutzernamen nicht enthalten.'
|
||||
} else {
|
||||
$minLen = 4
|
||||
if ($samLower.Length -ge $minLen) {
|
||||
for ($len = $minLen; $len -le $samLower.Length; $len++) {
|
||||
for ($start = 0; $start -le $samLower.Length - $len; $start++) {
|
||||
$sub = $samLower.Substring($start, $len)
|
||||
if ($pwLower -like "*${sub}*") {
|
||||
$errors += 'Passwort darf keine größeren Teile des Benutzernamens enthalten.'
|
||||
break 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors
|
||||
}
|
||||
# Ensure ActiveDirectory module available
|
||||
try {
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
} catch {
|
||||
$result.message = "ActiveDirectory PowerShell module not available: $($_.Exception.Message)"
|
||||
Write-Output ($result | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build New-ADUser parameters
|
||||
$props = @{
|
||||
Name = if ($display -and $display -ne '') { $display } else { $sam }
|
||||
SamAccountName = $sam
|
||||
Enabled = $true
|
||||
}
|
||||
|
||||
if ($mail -and $mail -ne '') { $props['EmailAddress'] = $mail }
|
||||
if ($ou -and $ou -ne '') { $props['Path'] = $ou }
|
||||
|
||||
# Validate password before continuing
|
||||
$pwErrors = Test-PasswordRequirements -Password $pass -SamAccountName $sam
|
||||
if ($pwErrors.Count -gt 0) {
|
||||
$result.message = 'Invalid password: ' + ($pwErrors -join ' | ')
|
||||
$result.hint = $passwordHint
|
||||
Write-Output ($result | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build secure password
|
||||
$securePass = ConvertTo-SecureString $pass -AsPlainText -Force
|
||||
$props['AccountPassword'] = $securePass
|
||||
|
||||
# Execute
|
||||
if ($dryRun) {
|
||||
$result.success = $true
|
||||
$result.message = "DRY RUN: would create user $($sam)"
|
||||
Write-Output ($result | ConvertTo-Json -Compress)
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
# Create the AD user
|
||||
New-ADUser @props -ErrorAction Stop
|
||||
|
||||
# Add to groups, if provided
|
||||
if ($groups -and $groups -ne '') {
|
||||
$groupList = $groups -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
||||
foreach ($g in $groupList) {
|
||||
Add-ADGroupMember -Identity $g -Members $sam -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
$result.success = $true
|
||||
$result.message = "User $($sam) created successfully"
|
||||
Write-Output ($result | ConvertTo-Json -Compress)
|
||||
exit 0
|
||||
} catch {
|
||||
$result.message = "Error creating user $($sam): $($_.Exception.Message)"
|
||||
Write-Output ($result | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$InputFile
|
||||
)
|
||||
|
||||
# Read meta JSON
|
||||
try {
|
||||
$json = Get-Content -Raw -Path $InputFile -ErrorAction Stop
|
||||
$meta = $json | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Output (@{ success = $false; message = "Failed to read/parse meta JSON: $($_.Exception.Message)" } | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$csvFile = [string]$meta.input_file
|
||||
# PowerShell 5.1 doesn't support the null-coalescing operator '??'.
|
||||
# Use an explicit check here to set the default delimiter.
|
||||
$delimiter = [string]$meta.delimiter
|
||||
if ([string]::IsNullOrWhiteSpace($delimiter)) { $delimiter = ',' }
|
||||
$hasHeader = [bool]($meta.has_header -as [bool])
|
||||
$dryRun = [bool]($meta.dry_run -as [bool])
|
||||
$defaultOu = [string]$meta.default_ou
|
||||
|
||||
|
||||
if (-not (Test-Path -Path $csvFile)) {
|
||||
Write-Output (@{ success = $false; message = "CSV file not found: $csvFile" } | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure ActiveDirectory module is available
|
||||
try {
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Output (@{ success = $false; message = "ActiveDirectory PowerShell module not available: $($_.Exception.Message)" } | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Read CSV
|
||||
try {
|
||||
if ($hasHeader) {
|
||||
$items = Import-Csv -Path $csvFile -Delimiter $delimiter -ErrorAction Stop
|
||||
} else {
|
||||
# Use default headers
|
||||
$headers = 'samaccountname','displayname','mail','password','groups'
|
||||
$items = Import-Csv -Path $csvFile -Delimiter $delimiter -Header $headers -ErrorAction Stop
|
||||
}
|
||||
} catch {
|
||||
Write-Output (@{ success = $false; message = "Failed to parse CSV: $($_.Exception.Message)" } | ConvertTo-Json -Compress)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$actor = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$results = @()
|
||||
$successCount = 0
|
||||
$failCount = 0
|
||||
|
||||
foreach ($row in $items) {
|
||||
$sam = $row.samaccountname
|
||||
$display = $row.displayname
|
||||
$mail = $row.mail
|
||||
$pass = $row.password
|
||||
$groups = $row.groups
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ou) -and -not [string]::IsNullOrWhiteSpace($defaultOu)) {
|
||||
$ou = $defaultOu
|
||||
}
|
||||
|
||||
# Password hint text (German)
|
||||
$passwordHint = @"
|
||||
Das sollten die Anforderungen an das Passwort sein:
|
||||
|
||||
- mindestens 7 Zeichen
|
||||
- darf den Benutzer-/Accountnamen nicht enthalten (bzw. keine zu großen Teile davon)
|
||||
- muss Zeichen aus mindestens 3 von 4 Kategorien enthalten:
|
||||
1) Großbuchstaben (A–Z)
|
||||
2) Kleinbuchstaben (a–z)
|
||||
3) Ziffern (0–9)
|
||||
4) Sonderzeichen (alles, was kein Buchstabe/Zahl ist, z. B. ! ? # _ - . , usw.)
|
||||
"@
|
||||
|
||||
function Test-PasswordRequirements {
|
||||
param(
|
||||
[string]$Password,
|
||||
[string]$SamAccountName
|
||||
)
|
||||
|
||||
$errors = @()
|
||||
|
||||
if ([string]::IsNullOrEmpty($Password) -or $Password.Length -lt 7) {
|
||||
$errors += 'Passwort muss mindestens 7 Zeichen lang sein.'
|
||||
}
|
||||
|
||||
# Categories: uppercase, lowercase, digit, special
|
||||
$categories = 0
|
||||
if ($Password -match '[A-Z]') { $categories++ }
|
||||
if ($Password -match '[a-z]') { $categories++ }
|
||||
if ($Password -match '\d') { $categories++ }
|
||||
if ($Password -match '[^A-Za-z0-9]') { $categories++ }
|
||||
if ($categories -lt 3) {
|
||||
$errors += 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten (Groß, Klein, Ziffern, Sonderzeichen).'
|
||||
}
|
||||
|
||||
# Check for username inclusion or large parts of it (case-insensitive)
|
||||
if (-not [string]::IsNullOrEmpty($SamAccountName)) {
|
||||
$pwLower = $Password.ToLowerInvariant()
|
||||
$samLower = $SamAccountName.ToLowerInvariant()
|
||||
|
||||
if ($pwLower -like "*${samLower}*") {
|
||||
$errors += 'Passwort darf den Benutzernamen nicht enthalten.'
|
||||
} else {
|
||||
# Check for substrings of username of length >= 4
|
||||
$minLen = 4
|
||||
if ($samLower.Length -ge $minLen) {
|
||||
for ($len = $minLen; $len -le $samLower.Length; $len++) {
|
||||
for ($start = 0; $start -le $samLower.Length - $len; $start++) {
|
||||
$sub = $samLower.Substring($start, $len)
|
||||
if ($pwLower -like "*${sub}*") {
|
||||
$errors += 'Passwort darf keine größeren Teile des Benutzernamens enthalten.'
|
||||
break 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($sam) -or [string]::IsNullOrWhiteSpace($pass)) {
|
||||
$results += @{ sam = $sam; success = $false; message = 'Missing samaccountname or password' }
|
||||
$failCount++
|
||||
continue
|
||||
}
|
||||
|
||||
# Validate password according to requirements, also during dry run so issues are visible early
|
||||
$pwErrors = Test-PasswordRequirements -Password $pass -SamAccountName $sam
|
||||
if ($pwErrors.Count -gt 0) {
|
||||
$results += @{ sam = $sam; success = $false; message = ('Invalid password: ' + ($pwErrors -join ' | ')); hint = $passwordHint }
|
||||
$failCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$results += @{ sam = $sam; success = $true; message = 'DRY RUN: would create (password validated)' }
|
||||
$successCount++
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
$props = @{ Name = if ($display -and $display -ne '') { $display } else { $sam }; SamAccountName = $sam; Enabled = $true }
|
||||
if ($mail -and $mail -ne '') { $props['EmailAddress'] = $mail }
|
||||
if ($ou -and $ou -ne '') { $props['Path'] = $ou }
|
||||
$securePass = ConvertTo-SecureString $pass -AsPlainText -Force
|
||||
$props['AccountPassword'] = $securePass
|
||||
|
||||
New-ADUser @props -ErrorAction Stop
|
||||
|
||||
if ($groups -and $groups -ne '') {
|
||||
$groupList = $groups -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
|
||||
foreach ($g in $groupList) {
|
||||
Add-ADGroupMember -Identity $g -Members $sam -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
$results += @{ sam = $sam; success = $true; message = 'Created' }
|
||||
$successCount++
|
||||
} catch {
|
||||
$results += @{ sam = $sam; success = $false; message = $_.Exception.Message }
|
||||
$failCount++
|
||||
}
|
||||
}
|
||||
|
||||
$output = @{ success = $failCount -eq 0; message = "Created $successCount users, $failCount failures"; details = $results; actor = $actor }
|
||||
Write-Output ($output | ConvertTo-Json -Compress)
|
||||
exit 0
|
||||
Loading…
Reference in New Issue
Block a user