Dienstag, 20 Januar 2026

Diese Woche am beliebtesten

Vertiefendes Material

InfinityChat

Der Dreamcodes InfinityChat ist eine leistungsstarke und zugleich leicht installierbare Messanger Komplettlösung für Echtzeit-Kommunikation. Entwickelt mit modernsten Web-Technologien bietet es eine sichere, flexible und optisch ansprechende Plattform, die sowohl für kleine Teams als auch für große Unternehmen geeignet ist.

Mit umfassenden Funktionen wie WebSocket-Echtzeitkommunikation, Chaträumen, Dateiuploads und Admin-Dashboard ermöglicht InfinityChat eine nahtlose und professionelle Chat-Erfahrung. Sicherheit wird großgeschrieben durch verschlüsselte Passwörter, Zwei-Faktor-Authentifizierung, CSRF-Schutz und strikte CORS-Regeln.

Die Plattform lässt sich einfach an Unternehmensumgebungen anpassen, unterstützt LDAP/SSO und bietet ein durchdachtes Rollen- und Rechtekonzept für Benutzer, Moderatoren und Administratoren.

Hauptfunktionen im Überblick

  • One-File-Installation
    Nach dem ersten Start erstellt das Script automatisch alle erforderlichen Dateien und Verzeichnisse. Keine komplizierte Einrichtung notwendig.
  • Modernes Design
    Professionelles UI/UX, responsive für Desktop, Tablet und Smartphone.
  • Sichere Anmeldung & Authentifizierung
    • Verschlüsselte Passwörter (bcrypt)
    • Zwei-Faktor-Authentifizierung (TOTP z. B. Google Authenticator)
    • E-Mail-gestützte Backup-Codes für Notfälle
    • Optional LDAP/SSO-Anbindung für Unternehmensumgebungen
  • Echtzeit-Kommunikation mit WebSockets
    Nachrichten werden sofort übertragen – keine Verzögerung, kein Polling.
    Unterstützt sowohl TCP (ws/wss) als auch Unix Domain Sockets für höchste Performance.
  • Chaträume & Gruppen
    • Öffentliche und private Räume
    • Direktnachrichten
    • Moderationsrechte für einzelne Räume
  • Dateiverwaltung & Uploads
    • Persistente Dateispeicherung
    • Automatische Thumbnail-Erstellung für Bilder
    • Upload-Filter (Typ & Größe) für maximale Sicherheit
  • Rollen- und Rechtekonzept
    • Benutzer: Chatten und Dateien austauschen
    • Moderatoren: Inhalte prüfen, Meldungen bearbeiten
    • Admins: Verwaltung von Nutzern, Räumen, Moderationsreports
  • Admin Dashboard
    • Übersichtliche Benutzerverwaltung
    • Live-Statistiken: aktive Nutzer, Nachrichtenanzahl, Speicherverbrauch
    • Reportsystem: gemeldete Nachrichten einsehen und bearbeiten
    • Automatische Moderation gegen verbotene Wörter / Spam
  • Sicherheit & Datenschutz
    • CSRF-Schutz bei Formularen
    • Strikte CORS-Regeln für Produktiv-Domains
    • Prepared Statements in allen SQL-Abfragen
    • HTTPS-Unterstützung für wss-Verbindungen

Einsatzmöglichkeiten

  • Unternehmen: Interne Kommunikation, Projektteams, schnelle Abstimmung
  • Communities: Eigene Chat-Server mit Gruppen- und Admin-Rechten
  • Kunden-Support: Direkter Support-Chat mit Rollen- und Rechteverwaltung
  • Bildungseinrichtungen: Virtuelle Arbeitsgruppen, Kurs-Chats, Projektkommunikation

Highlights für Unternehmen

  • Single-Sign-On & LDAP-Unterstützung
  • Erweiterbare API-Schnittstelle
  • Rollenbasiertes Berechtigungsmodell
  • Vollständige Kontrolle über Datenhaltung und Hosting
<?php
// configuration
$db_host = 'localhost';
$db_user = 'root';
$db_pass = '';
$db_name = 'chat_app';
$upload_max_filesize_bytes = 10 * 1024 * 1024; // 10MB
$allowed_upload_types = [
    'image/jpeg', 'image/png', 'image/gif',
    'application/pdf', 'text/plain'
];
$production_origins = [
    'https://example.com'
];
$site_footer = 'http://www.deinedomain.tld';
$ipc_socket = __DIR__ . '/tmp/chat_ipc.sock'; // unix domain socket path for IPC between PHP and WS server

// moderation config
$banned_words = ['badword', 'violation', 'spamword']; // einfache Liste, anpassen
$auto_flag_threshold = 1; // number of matches to auto flag

// helpers
function create_file($path, $content) {
    $dir = dirname($path);
    if (!is_dir($dir)) mkdir($dir, 0755, true);
    file_put_contents($path, $content);
}

// first run installer
if (!file_exists(__DIR__ . '/.installed')) {
    @mkdir(__DIR__ . '/json', 0755, true);
    @mkdir(__DIR__ . '/uploads', 0755, true);
    @mkdir(__DIR__ . '/tmp', 0755, true);

    if (!file_exists(__DIR__ . '/json/messages.json')) file_put_contents(__DIR__ . '/json/messages.json', json_encode([]));
    if (!file_exists(__DIR__ . '/json/users.json')) file_put_contents(__DIR__ . '/json/users.json', json_encode([]));

    // websocket server for CLI listens on TCP for browsers and on UNIX socket for IPC from PHP
    $ws = <<<'PHP'
<?php
// Starten via CLI: php ws_server.php
set_time_limit(0);
$tcpAddress = '0.0.0.0';
$tcpPort = 9000;
$unixPath = __DIR__ . '/tmp/chat_ipc.sock';
if (file_exists($unixPath)) @unlink($unixPath);

// create tcp socket for browser connections
$tcp = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($tcp, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($tcp, $tcpAddress, $tcpPort);
socket_listen($tcp);

// create unix domain socket for IPC from PHP
$unix = socket_create(AF_UNIX, SOCK_STREAM, 0);
socket_bind($unix, $unixPath);
socket_listen($unix);

$clients = [$tcp, $unix];
$meta = []; // metadata for tcp client sockets keyed by resource id

function send_tcp($client, $msg){
    @socket_write($client, $msg . "
");
}

function broadcast_room($clientsMeta, $room, $msg){
    foreach ($clientsMeta as $cid => $info){
        if (($info['room'] ?? null) === $room && isset($info['socket'])){
            @socket_write($info['socket'], $msg . "
");
        }
    }
}

while (true){
    $read = $clients;
    // add client sockets to read array
    foreach ($clients as $c){ if (is_resource($c)) continue; }
    $read = $clients;
    $all = $read;
    // also add dynamic client sockets
    foreach ($all as $s){ $read[] = $s; }
    $write = null; $except = null;
    if (socket_select($read, $write, $except, 1) < 1) continue;
    foreach ($read as $sock){
        if ($sock === $tcp){
            $newsock = socket_accept($tcp);
            if ($newsock){
                $id = intval($newsock);
                $clients[] = $newsock;
                $meta[$id] = ['socket' => $newsock, 'room' => 'global', 'username' => null];
                send_tcp($newsock, json_encode(['type' => 'welcome', 'msg' => 'connected']));
            }
            continue;
        }
        if ($sock === $unix){
            $uconn = socket_accept($unix);
            // read one line json from PHP and process broadcast
            $data = '';
            while ($b = socket_read($uconn, 2048)){
                $data .= $b;
                if (substr($b, -1) === "
") break;
            }
            $data = trim($data);
            if ($data){
                $p = json_decode($data, true);
                if (is_array($p) && isset($p['type']) && $p['type'] === 'message'){
                    broadcast_room($meta, $p['room'] ?? 'global', json_encode(['type' => 'message', 'room' => $p['room'] ?? 'global', 'username' => $p['username'] ?? 'system', 'message' => $p['message'] ?? '', 'time' => date('c')]));
                }
            }
            socket_close($uconn);
            continue;
        }
        // existing tcp client sent data
        $id = intval($sock);
        $data = @socket_read($sock, 4096, PHP_NORMAL_READ);
        if ($data === false || $data === ''){
            if (isset($meta[$id]['socket'])) socket_close($meta[$id]['socket']);
            unset($meta[$id]);
            $key = array_search($sock, $clients, true);
            if ($key !== false) unset($clients[$key]);
            continue;
        }
        $data = trim($data);
        if (!$data) continue;
        $p = json_decode($data, true);
        if (!is_array($p)) continue;
        if ($p['type'] === 'join'){
            $meta[$id]['room'] = $p['room'] ?? 'global';
            $meta[$id]['username'] = $p['username'] ?? null;
            send_tcp($sock, json_encode(['type' => 'joined', 'room' => $meta[$id]['room']]));
        }
        if ($p['type'] === 'message'){
            $room = $p['room'] ?? 'global';
            broadcast_room($meta, $room, json_encode(['type' => 'message', 'room' => $room, 'username' => $meta[$id]['username'] ?? 'anon', 'message' => $p['message'] ?? '', 'time' => date('c')]));
        }
    }
}
PHP;
    create_file(__DIR__ . '/ws_server.php', $ws);

    // API stub
    create_file(__DIR__ . '/api.php', "<?php require_once __DIR__ . '/index.php'; exit; ");

    // create DB and tables with extra columns for TOTP, backup codes, reports
    $mysqli = @new mysqli($db_host, $db_user, $db_pass);
    if ($mysqli && !$mysqli->connect_error){
        $mysqli->query("CREATE DATABASE IF NOT EXISTS `" . $db_name . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
        $mysqli->select_db($db_name);
        $mysqli->query("CREATE TABLE IF NOT EXISTS users (
            id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            username VARCHAR(100) NOT NULL UNIQUE,
            displayname VARCHAR(150) NOT NULL,
            password_hash VARCHAR(255) NOT NULL,
            role ENUM('user','moderator','admin') NOT NULL DEFAULT 'user',
            totp_secret VARCHAR(64) NULL,
            totp_enabled TINYINT(1) DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $mysqli->query("CREATE TABLE IF NOT EXISTS rooms (
            id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(150) NOT NULL UNIQUE,
            displayname VARCHAR(255) NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $mysqli->query("CREATE TABLE IF NOT EXISTS messages (
            id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            room VARCHAR(150) NOT NULL DEFAULT 'global',
            sender_id INT UNSIGNED NULL,
            sender_username VARCHAR(100) NULL,
            message TEXT NULL,
            attachment VARCHAR(255) NULL,
            flagged TINYINT(1) DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $mysqli->query("CREATE TABLE IF NOT EXISTS reports (
            id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            message_id INT UNSIGNED NULL,
            reporter_id INT UNSIGNED NULL,
            reason VARCHAR(255) NULL,
            handled TINYINT(1) DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $mysqli->query("CREATE TABLE IF NOT EXISTS backup_codes (
            id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id INT UNSIGNED NOT NULL,
            code_hash VARCHAR(255) NOT NULL,
            used TINYINT(1) DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $mysqli->query("INSERT IGNORE INTO rooms (name, displayname) VALUES ('global','Allgemeiner Raum')");
        $mysqli->close();
    }

    file_put_contents(__DIR__ . '/.installed', 'installed on ' . date('c'));
    header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
    exit;
}

session_start();

// CORS strict rules
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin && in_array($origin, $production_origins, true)){
    header('Access-Control-Allow-Origin: ' . $origin);
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
    header('Access-Control-Allow-Headers: Content-Type, X-CSRF-Token');
} else {
    // for local testing allow localhost
    if ($origin && (strpos($origin, 'localhost') !== false || strpos($origin, '127.0.0.1') !== false)){
        header('Access-Control-Allow-Origin: ' . $origin);
        header('Access-Control-Allow-Credentials: true');
        header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
        header('Access-Control-Allow-Headers: Content-Type, X-CSRF-Token');
    }
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;

$action = $_REQUEST['action'] ?? 'ui';

function db(){
    global $db_host, $db_user, $db_pass, $db_name;
    $m = new mysqli($db_host, $db_user, $db_pass, $db_name);
    if ($m->connect_error){ http_response_code(500); echo json_encode(['error' => 'Datenbankverbindung fehlgeschlagen']); exit; }
    $m->set_charset('utf8mb4');
    return $m;
}

// csrf helpers
function csrf_token(){
    if (empty($_SESSION['csrf_token'])){
        $_SESSION['csrf_token'] = bin2hex(random_bytes(16));
    }
    return $_SESSION['csrf_token'];
}
function csrf_check(){
    $header = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
    $post = $_POST['_csrf'] ?? null;
    $token = $header ?: $post;
    if (!$token || !hash_equals($_SESSION['csrf_token'] ?? '', $token)){
        http_response_code(403);
        echo json_encode(['ok' => false, 'msg' => 'Ungültiges CSRF Token']);
        exit;
    }
}

// sanitizers
function clean_username($s){ $s = trim($s); $s = preg_replace('/[^a-zA-Z0-9_\.\@-]/u', '', $s); return substr($s,0,100); }
function clean_text($s, $max = 5000){ $s = trim($s); $s = strip_tags($s); return substr($s,0,$max); }

// TOTP helpers base32 and TOTP algorithm
function base32_encode_secret($bytes){
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $bits = '';
    foreach (str_split($bytes) as $c) $bits .= str_pad(decbin(ord($c)), 8, '0', STR_PAD_LEFT);
    $out = '';
    while (strlen($bits) >= 5){ $chunk = substr($bits,0,5); $bits = substr($bits,5); $out .= $alphabet[bindec($chunk)]; }
    if (strlen($bits) > 0) $out .= $alphabet[bindec(str_pad($bits,5,'0'))];
    return $out;
}
function hotp($secret, $counter){
    $key = base32_decode($secret);
    $bin_counter = pack('N*', 0) . pack('N*', $counter);
    $hash = hash_hmac('sha1', $bin_counter, $key, true);
    $offset = ord($hash[19]) & 0xf;
    $code = (ord($hash[$offset]) & 0x7f) << 24 | (ord($hash[$offset+1]) & 0xff) << 16 | (ord($hash[$offset+2]) & 0xff) << 8 | (ord($hash[$offset+3]) & 0xff);
    return $code % 1000000;
}
function totp($secret, $timeSlice = null){
    if ($timeSlice === null) $timeSlice = floor(time() / 30);
    return str_pad(hotp($secret, $timeSlice), 6, '0', STR_PAD_LEFT);
}
function base32_decode($b32){
    $b32 = strtoupper($b32);
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $bits = '';
    foreach (str_split($b32) as $c){ $val = strpos($alphabet, $c); if ($val === false) continue; $bits .= str_pad(decbin($val),5,'0',STR_PAD_LEFT); }
    $bytes = '';
    while (strlen($bits) >= 8){ $chunk = substr($bits,0,8); $bits = substr($bits,8); $bytes .= chr(bindec($chunk)); }
    return $bytes;
}

// optional LDAP auth
$ldap_enabled = false;
$ldap_server = 'ldap://ldap.example.com';
$ldap_base_dn = 'ou=users,dc=example,dc=com';
function ldap_auth($user, $pass){
    global $ldap_server, $ldap_base_dn;
    if (!extension_loaded('ldap')) return false;
    $ldap = ldap_connect($ldap_server);
    ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
    $filter = "(uid=" . ldap_escape($user, '', LDAP_ESCAPE_FILTER) . ")";
    $res = ldap_search($ldap, $ldap_base_dn, $filter);
    $entries = ldap_get_entries($ldap, $res);
    if (empty($entries[0])) return false;
    $dn = $entries[0]['dn'];
    return @ldap_bind($ldap, $dn, $pass);
}

// helper: generate backup codes
function generate_backup_codes($count = 6){
    $codes = [];
    for ($i=0;$i<$count;$i++){
        $codes[] = substr(bin2hex(random_bytes(4)),0,8);
    }
    return $codes;
}
function store_backup_codes($user_id, $codes){
    $mysqli = db();
    $stmt = $mysqli->prepare('INSERT INTO backup_codes (user_id, code_hash) VALUES (?, ?)');
    foreach ($codes as $c){ $hash = password_hash($c, PASSWORD_DEFAULT); $stmt->bind_param('is', $user_id, $hash); $stmt->execute(); }
    $stmt->close(); $mysqli->close();
}
function consume_backup_code($user_id, $code){
    $mysqli = db();
    $res = $mysqli->query('SELECT id, code_hash FROM backup_codes WHERE user_id = ' . intval($user_id) . ' AND used = 0');
    while ($r = $res->fetch_assoc()){
        if (password_verify($code, $r['code_hash'])){
            $stmt = $mysqli->prepare('UPDATE backup_codes SET used = 1 WHERE id = ?'); $stmt->bind_param('i', $r['id']); $stmt->execute(); $stmt->close(); $mysqli->close(); return true;
        }
    }
    $mysqli->close(); return false;
}

// endpoints
if ($action === 'register'){
    csrf_check();
    $username = clean_username($_POST['username'] ?? '');
    $display = trim($_POST['display'] ?? $username);
    $password = $_POST['password'] ?? '';
    if ($username === '' || $password === ''){ echo json_encode(['ok' => false, 'msg' => 'Benutzername und Passwort erforderlich']); exit; }
    if (strlen($password) < 8){ echo json_encode(['ok' => false, 'msg' => 'Passwort zu kurz']); exit; }
    $hash = password_hash($password, PASSWORD_DEFAULT);
    $mysqli = db();
    $stmt = $mysqli->prepare('INSERT INTO users (username, displayname, password_hash) VALUES (?, ?, ?)');
    $stmt->bind_param('sss', $username, $display, $hash);
    $ok = $stmt->execute();
    if (!$ok){ echo json_encode(['ok' => false, 'msg' => 'Benutzer existiert bereits oder fehler']); $stmt->close(); $mysqli->close(); exit; }
    $userid = $mysqli->insert_id;
    $stmt->close(); $mysqli->close();
    $_SESSION['user_id'] = $userid; $_SESSION['username'] = $username; $_SESSION['displayname'] = $display; $_SESSION['role'] = 'user';
    echo json_encode(['ok' => true, 'username' => $username, 'display' => $display]); exit;
}

if ($action === 'login'){
    $username = clean_username($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    // sso header check
    $sso_user = $_SERVER['HTTP_X_SSO_USER'] ?? null;
    if ($sso_user){
        $username = clean_username($sso_user);
        // trust sso then create session if user exists
        $mysqli = db();
        $stmt = $mysqli->prepare('SELECT id, username, displayname, role, totp_enabled FROM users WHERE username = ? LIMIT 1');
        $stmt->bind_param('s', $username);
        $stmt->execute(); $res = $stmt->get_result();
        if ($row = $res->fetch_assoc()){
            $_SESSION['user_id'] = $row['id']; $_SESSION['username'] = $row['username']; $_SESSION['displayname'] = $row['displayname']; $_SESSION['role'] = $row['role'];
            echo json_encode(['ok' => true]); $stmt->close(); $mysqli->close(); exit;
        }
        $stmt->close(); $mysqli->close();
    }
    // ldap fallback
    global $ldap_enabled;
    if ($ldap_enabled){
        if (ldap_auth($username, $password)){
            // create local user if not exists
            $mysqli = db();
            $stmt = $mysqli->prepare('SELECT id, username, displayname, role FROM users WHERE username = ? LIMIT 1');
            $stmt->bind_param('s', $username); $stmt->execute(); $res = $stmt->get_result();
            if ($row = $res->fetch_assoc()){
                $_SESSION['user_id'] = $row['id']; $_SESSION['username'] = $row['username']; $_SESSION['displayname'] = $row['displayname']; $_SESSION['role'] = $row['role'];
                echo json_encode(['ok' => true]); $stmt->close(); $mysqli->close(); exit;
            } else {
                $display = $username;
                $pw = password_hash(random_bytes(16), PASSWORD_DEFAULT);
                $ins = $mysqli->prepare('INSERT INTO users (username, displayname, password_hash) VALUES (?, ?, ?)');
                $ins->bind_param('sss', $username, $display, $pw); $ins->execute();
                $_SESSION['user_id'] = $ins->insert_id; $_SESSION['username'] = $username; $_SESSION['displayname'] = $display; $_SESSION['role'] = 'user';
                echo json_encode(['ok' => true]); $ins->close(); $mysqli->close(); exit;
            }
        }
    }
    // local login
    $mysqli = db();
    $stmt = $mysqli->prepare('SELECT id, username, displayname, password_hash, role, totp_enabled, totp_secret FROM users WHERE username = ? LIMIT 1');
    $stmt->bind_param('s', $username); $stmt->execute(); $res = $stmt->get_result();
    if ($row = $res->fetch_assoc()){
        if (password_verify($password, $row['password_hash'])){
            // if user has totp enabled require code
            if (intval($row['totp_enabled']) === 1){
                // store temp flag and prompt for code
                $_SESSION['tmp_user_id'] = $row['id']; $_SESSION['tmp_username'] = $row['username'];
                echo json_encode(['ok' => true, 'require_totp' => true]);
            } else {
                $_SESSION['user_id'] = $row['id']; $_SESSION['username'] = $row['username']; $_SESSION['displayname'] = $row['displayname']; $_SESSION['role'] = $row['role'];
                echo json_encode(['ok' => true]);
            }
        } else echo json_encode(['ok' => false, 'msg' => 'Ungültige Zugangsdaten']);
    } else echo json_encode(['ok' => false, 'msg' => 'Ungültige Zugangsdaten']);
    $stmt->close(); $mysqli->close(); exit;
}

// verify totp code after password
if ($action === 'verify_totp'){
    $code = trim($_POST['code'] ?? '');
    if (empty($_SESSION['tmp_user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Keine Authentifizierung ausstehend']); exit; }
    $uid = intval($_SESSION['tmp_user_id']);
    $mysqli = db();
    $stmt = $mysqli->prepare('SELECT id, username, totp_secret FROM users WHERE id = ? LIMIT 1');
    $stmt->bind_param('i', $uid); $stmt->execute(); $res = $stmt->get_result();
    if ($row = $res->fetch_assoc()){
        $secret = $row['totp_secret'];
        if ($secret && (totp($secret) === $code || consume_backup_code($uid, $code))){
            // complete login
            $_SESSION['user_id'] = $row['id']; $_SESSION['username'] = $row['username']; // load role
            $r2 = $mysqli->prepare('SELECT role, displayname FROM users WHERE id = ?'); $r2->bind_param('i', $uid); $r2->execute(); $rr = $r2->get_result()->fetch_assoc(); $_SESSION['role'] = $rr['role']; $_SESSION['displayname'] = $rr['displayname'];
            unset($_SESSION['tmp_user_id'], $_SESSION['tmp_username']);
            echo json_encode(['ok' => true]);
        } else echo json_encode(['ok' => false, 'msg' => 'Ungültiger Code oder Backup Code']);
    } else echo json_encode(['ok' => false, 'msg' => 'Benutzer nicht gefunden']);
    $stmt->close(); $mysqli->close(); exit;
}

// generate and email backup codes
if ($action === 'generate_backup_codes'){
    if (!isset($_SESSION['user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Nicht angemeldet']); exit; }
    csrf_check();
    $uid = intval($_SESSION['user_id']);
    $codes = generate_backup_codes(8);
    store_backup_codes($uid, $codes);
    // try to send email to user - assumes users have email column; if not, admin can fetch codes
    $mysqli = db(); $stmt = $mysqli->prepare('SELECT displayname FROM users WHERE id = ?'); $stmt->bind_param('i', $uid); $stmt->execute(); $res = $stmt->get_result(); $emailtext = implode("
", $codes); $stmt->close(); $mysqli->close();
    // NOTE: mail() may not be configured. Adjust to your mail system.
    @mail('admin@example.com', 'Your Backup Codes', "Backup Codes:

" . $emailtext);
    echo json_encode(['ok' => true, 'codes' => $codes]); exit;
}

if ($action === 'logout'){
    session_unset(); session_destroy(); echo json_encode(['ok' => true]); exit;
}

// upload with thumbnail generation
if ($action === 'upload'){
    if (!isset($_SESSION['user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Nicht angemeldet']); exit; }
    csrf_check();
    if (empty($_FILES['file'])){ echo json_encode(['ok' => false, 'msg' => 'Keine Datei']); exit; }
    $file = $_FILES['file']; if ($file['error'] !== UPLOAD_ERR_OK){ echo json_encode(['ok' => false, 'msg' => 'Upload Fehler']); exit; }
    global $upload_max_filesize_bytes, $allowed_upload_types;
    if ($file['size'] > $upload_max_filesize_bytes){ echo json_encode(['ok' => false, 'msg' => 'Datei zu groß']); exit; }
    $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo);
    if (!in_array($mime, $allowed_upload_types, true)){ echo json_encode(['ok' => false, 'msg' => 'Dateityp nicht erlaubt']); exit; }
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $safe = bin2hex(random_bytes(12)) . '.' . $ext; $dest = __DIR__ . '/uploads/' . $safe;
    if (!move_uploaded_file($file['tmp_name'], $dest)){ echo json_encode(['ok' => false, 'msg' => 'Konnte Datei nicht speichern']); exit; }
    $thumb = null;
    if (strpos($mime, 'image/') === 0 && extension_loaded('gd')){
        $thumbname = 'thumb_' . $safe;
        $thumbpath = __DIR__ . '/uploads/' . $thumbname;
        list($w, $h) = getimagesize($dest);
        $nw = 240; $nh = intval($h * $nw / $w);
        $dst = imagecreatetruecolor($nw, $nh);
        if ($mime === 'image/jpeg'){ $src = imagecreatefromjpeg($dest); }
        elseif ($mime === 'image/png'){ $src = imagecreatefrompng($dest); }
        elseif ($mime === 'image/gif'){ $src = imagecreatefromgif($dest); }
        else $src = null;
        if ($src){ imagecopyresampled($dst, $src, 0,0,0,0, $nw, $nh, $w, $h); imagejpeg($dst, $thumbpath, 80); imagedestroy($dst); imagedestroy($src); $thumb = 'uploads/' . $thumbname; }
    }
    echo json_encode(['ok' => true, 'file' => 'uploads/' . $safe, 'thumb' => $thumb, 'mime' => $mime]); exit;
}

// automatic moderation check helper
function moderation_check($text){
    global $banned_words, $auto_flag_threshold;
    $found = 0;
    foreach ($banned_words as $w){ if (stripos($text, $w) !== false) $found++; }
    return $found >= $auto_flag_threshold;
}

// send message with IPC to websocket server and moderation & reporting
if ($action === 'send'){
    if (!isset($_SESSION['user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Nicht angemeldet']); exit; }
    csrf_check();
    $room = clean_username($_POST['room'] ?? 'global');
    $msg = clean_text($_POST['msg'] ?? '');
    $attachment = null; if (!empty($_POST['attachment'])) $attachment = basename($_POST['attachment']);
    if ($msg === '' && $attachment === null){ echo json_encode(['ok' => false, 'msg' => 'Nachricht leer']); exit; }
    $flagged = moderation_check($msg) ? 1 : 0;
    $mysqli = db();
    $stmt = $mysqli->prepare('INSERT INTO messages (room, sender_id, sender_username, message, attachment, flagged) VALUES (?, ?, ?, ?, ?, ?)');
    $uid = intval($_SESSION['user_id']); $uname = $_SESSION['username'];
    $stmt->bind_param('sisssi', $room, $uid, $uname, $msg, $attachment, $flagged);
    $stmt->execute(); $id = $mysqli->insert_id; $stmt->close();
    if ($flagged){
        $rep = $mysqli->prepare('INSERT INTO reports (message_id, reporter_id, reason) VALUES (?, ?, ?)');
        $reason = 'Automatische Moderation'; $rep->bind_param('iis', $id, $uid, $reason); $rep->execute(); $rep->close();
    }
    $mysqli->close();
    // json backup
    $file = __DIR__ . '/json/messages.json'; $arr = json_decode(file_get_contents($file), true); if (!is_array($arr)) $arr = [];
    $arr[] = ['id' => $id, 'room' => $room, 'sender_id' => $uid, 'sender_username' => $uname, 'message' => $msg, 'attachment' => $attachment, 'flagged' => $flagged, 'created_at' => date('c')];
    file_put_contents($file, json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    // send notification to websocket server via unix domain socket if available
    global $ipc_socket;
    if (file_exists($ipc_socket)){
        $sock = socket_create(AF_UNIX, SOCK_STREAM, 0);
        if ($sock && @socket_connect($sock, $ipc_socket)){
            $payload = json_encode(['type' => 'message', 'room' => $room, 'username' => $uname, 'message' => $msg]);
            @socket_write($sock, $payload . "
");
            socket_close($sock);
        }
    }
    echo json_encode(['ok' => true, 'id' => $id, 'flagged' => $flagged]); exit;
}

// report message endpoint (user reports a message)
if ($action === 'report'){
    if (!isset($_SESSION['user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Nicht angemeldet']); exit; }
    csrf_check(); $mid = intval($_POST['message_id'] ?? 0); $reason = clean_text($_POST['reason'] ?? ''); if (!$mid) { echo json_encode(['ok' => false]); exit; }
    $mysqli = db(); $stmt = $mysqli->prepare('INSERT INTO reports (message_id, reporter_id, reason) VALUES (?, ?, ?)'); $uid = intval($_SESSION['user_id']); $stmt->bind_param('iis', $mid, $uid, $reason); $stmt->execute(); $stmt->close(); $mysqli->close(); echo json_encode(['ok' => true]); exit;
}

// fetch messages
if ($action === 'fetch'){
    if (!isset($_SESSION['user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Nicht angemeldet']); exit; }
    $room = clean_username($_GET['room'] ?? 'global'); $limit = intval($_GET['limit'] ?? 200);
    $mysqli = db();
    $stmt = $mysqli->prepare('SELECT id, room, sender_username, message, attachment, flagged, created_at FROM messages WHERE room = ? ORDER BY id DESC LIMIT ?');
    $stmt->bind_param('si', $room, $limit); $stmt->execute(); $res = $stmt->get_result(); $out = [];
    while ($r = $res->fetch_assoc()) $out[] = $r; $stmt->close(); $mysqli->close();
    echo json_encode(array_reverse($out)); exit;
}

// rooms
if ($action === 'rooms'){
    $mysqli = db(); $res = $mysqli->query('SELECT name, displayname FROM rooms ORDER BY displayname ASC'); $out = [];
    while ($r = $res->fetch_assoc()) $out[] = $r; $mysqli->close(); echo json_encode($out); exit;
}

// admin endpoints
function require_role($role){ if (!isset($_SESSION['role'])){ http_response_code(403); echo json_encode(['ok' => false, 'msg' => 'Forbidden']); exit; } if ($role === 'admin' && $_SESSION['role'] !== 'admin'){ http_response_code(403); echo json_encode(['ok' => false, 'msg' => 'Forbidden']); exit; } }
if ($action === 'admin_list_users'){
    require_role('admin'); $mysqli = db(); $res = $mysqli->query('SELECT id, username, displayname, role, totp_enabled, created_at FROM users ORDER BY created_at DESC LIMIT 1000'); $out = [];
    while ($r = $res->fetch_assoc()) $out[] = $r; $mysqli->close(); echo json_encode($out); exit;
}
if ($action === 'admin_delete_message'){
    require_role('admin'); csrf_check(); $id = intval($_POST['id'] ?? 0); if (!$id){ echo json_encode(['ok' => false]); exit; }
    $mysqli = db(); $stmt = $mysqli->prepare('DELETE FROM messages WHERE id = ?'); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->close(); $mysqli->close(); echo json_encode(['ok' => true]); exit;
}
// admin set role
if ($action === 'admin_set_role'){
    require_role('admin'); csrf_check(); $uid = intval($_POST['id'] ?? 0); $role = $_POST['role'] ?? 'user'; if (!in_array($role, ['user','moderator','admin'])) $role = 'user'; $mysqli = db(); $stmt = $mysqli->prepare('UPDATE users SET role = ? WHERE id = ?'); $stmt->bind_param('si', $role, $uid); $stmt->execute(); $stmt->close(); $mysqli->close(); echo json_encode(['ok' => true]); exit;
}
// admin reports list
if ($action === 'admin_reports'){
    require_role('admin'); $mysqli = db(); $res = $mysqli->query('SELECT r.id, r.message_id, r.reporter_id, r.reason, r.handled, r.created_at, m.message, m.sender_username FROM reports r LEFT JOIN messages m ON r.message_id = m.id ORDER BY r.created_at DESC LIMIT 500'); $out = [];
    while ($r = $res->fetch_assoc()) $out[] = $r; $mysqli->close(); echo json_encode($out); exit;
}
// admin mark report handled
if ($action === 'admin_handle_report'){
    require_role('admin'); csrf_check(); $id = intval($_POST['id'] ?? 0); if (!$id){ echo json_encode(['ok' => false]); exit; }
    $mysqli = db(); $stmt = $mysqli->prepare('UPDATE reports SET handled = 1 WHERE id = ?'); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->close(); $mysqli->close(); echo json_encode(['ok' => true]); exit;
}

// statistics endpoint for admin
if ($action === 'admin_stats'){
    require_role('admin'); $mysqli = db();
    $res1 = $mysqli->query('SELECT COUNT(*) AS users FROM users'); $users = $res1->fetch_assoc()['users'];
    $res2 = $mysqli->query("SELECT room, COUNT(*) AS cnt FROM messages GROUP BY room ORDER BY cnt DESC LIMIT 10"); $rooms = []; while ($r = $res2->fetch_assoc()) $rooms[] = $r;
    // storage usage
    $size = 0; foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/uploads', FilesystemIterator::SKIP_DOTS)) as $f){ $size += $f->getSize(); }
    $mysqli->close(); echo json_encode(['users' => intval($users), 'rooms' => $rooms, 'storage_bytes' => $size]); exit;
}

// ui main with admin dashboard embedded
if ($action === 'ui'){
    $me = $_SESSION['username'] ?? ''; $display = $_SESSION['displayname'] ?? $me; $csrf = csrf_token(); $role = $_SESSION['role'] ?? '';
    ?>
    <!doctype html>
    <html lang="de">
    <head>
        <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Dreamcodes InfinityChat</title>
        <style>
            :root{ --bg:#f5f7fb; --accent:#0b64d4; --card:#fff; --muted:#6b7280 }
            html,body{height:100%;margin:0;font-family:Inter,system-ui,Arial;background:var(--bg)}
            .wrap{display:grid;grid-template-columns:320px 1fr;height:100vh;gap:28px;padding:28px}
            .panel{background:var(--card);border-radius:12px;box-shadow:0 6px 18px rgba(15,23,42,0.06);overflow:hidden}
            .sidebar{padding:18px;height:calc(100vh - 56px);box-sizing:border-box}
            .brand h1{margin:0;font-size:18px}
            .contacts{overflow:auto;max-height:calc(100% - 180px)}
            .contact{padding:10px;border-radius:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
            .header{padding:18px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #eef2ff}
            .chatbox{padding:18px;flex:1;overflow:auto}
            .composer{padding:14px;border-top:1px solid #eef2ff;display:flex;gap:10px;align-items:center}
            .msg{max-width:70%;padding:10px 12px;border-radius:10px;margin:8px 0}
            .msg.me{margin-left:auto;background:linear-gradient(180deg,#daf6ff,#bfefff)}
            .msg.they{margin-right:auto;background:#f7f8fb}
            .footer{padding:10px 0;text-align:center;color:var(--muted);font-size:13px}
            .admin-panel{padding:16px}
            table{width:100%;border-collapse:collapse}
            th,td{padding:8px;border-bottom:1px solid #eee;text-align:left}
            .btn{padding:6px 8px;border-radius:6px;border:1px solid #ddd;background:#fff;cursor:pointer}
            .stats{display:flex;gap:12px;margin-bottom:12px}
            .stat-card{background:#f8fafc;padding:12px;border-radius:8px;flex:1}
        </style>
    </head>
    <body>
    <div class="wrap">
        <div class="panel sidebar">
            <div class="sidebar">
                <div class="brand"><div><h1>Dreamcodes InfinityChat</h1><div style="font-size:13px;color:#6b7280">Angemeldet als <strong id="meName"><?php echo htmlspecialchars($display); ?></strong></div></div></div>
                <div style="margin:12px 0"><input id="roomFilter" placeholder="Raum wählen oder neu erstellen" style="width:100%;padding:10px;border-radius:8px;border:1px solid #e6eefc"></div>
                <div style="display:flex;gap:8px;margin-bottom:10px"><button id="btnNewRoom" class="btn">Neuen Raum</button><button id="btnUsers" class="btn">Benutzer</button></div>
                <div class="contacts" id="roomList"></div>
                <?php if ($role === 'admin'){ echo '<div style="margin-top:12px"><button id="adminBtn" class="btn">Admin Bereich</button></div>'; } ?>
                <div style="margin-top:12px"><a href="#" id="logoutBtn">Abmelden</a></div>
            </div>
        </div>
        <div class="panel main">
            <div class="header"><div><h2 id="roomTitle">Wähle einen Raum</h2><div style="font-size:13px;color:#6b7280" id="roomSub">Tippe auf einen Raum um zu starten</div></div><div><input id="fileInput" type="file" style="display:none"><button id="attachBtn" class="btn">Anhang</button></div></div>
            <div class="chatbox" id="chatWindow"><div style="font-size:13px;color:#6b7280">Noch keine Unterhaltung</div></div>
            <div class="composer"><input type="text" id="msgInput" placeholder="Nachricht eingeben" style="flex:1;padding:10px;border-radius:8px;border:1px solid #e6eefc"><button id="sendBtn" class="btn">Senden</button></div>
            <div class="footer">
    © <?php echo date("Y"); ?> 
    <a href="https://www.dreamcodes.net" target="_blank">Dreamcodes</a> — 
    Besuche auch <a href="<?php echo $site_footer; ?>" target="_blank"><?php echo $site_footer; ?></a>
</div>
        </div>
    </div>
    <script>
        const csrf = '<?php echo $csrf; ?>';
        let me = '<?php echo addslashes($me); ?>'; let room = 'global'; let attachment = null; let role = '<?php echo $role; ?>';
        async function api(path, data, method = 'GET'){
            if (method === 'GET'){
                const q = new URLSearchParams(data).toString(); const res = await fetch(path + '?' + q, { credentials: 'include' }); return res.json();
            } else {
                const fd = new FormData(); for (const k in data) fd.append(k, data[k]); fd.append('_csrf', csrf); const res = await fetch(path, { method: 'POST', body: fd, credentials: 'include' }); return res.json();
            }
        }
        async function loadRooms(){ const r = await api('?', { action: 'rooms' }); const list = document.getElementById('roomList'); list.innerHTML = ''; r.forEach(x => { const el = document.createElement('div'); el.className = 'contact'; el.textContent = x.displayname + ' (' + x.name + ')'; el.onclick = () => { selectRoom(x.name, x.displayname); }; list.appendChild(el); }); }
        async function selectRoom(name, display){ room = name; document.getElementById('roomTitle').textContent = display; fetchMessages(); joinWsRoom(); }
        async function fetchMessages(){ const res = await api('?', { action: 'fetch', room: room, limit: 200 }); const w = document.getElementById('chatWindow'); w.innerHTML = ''; res.forEach(m => { const d = document.createElement('div'); d.className = 'msg ' + ((m.sender_username === me) ? 'me' : 'they'); let html = '<div style="font-size:0.85rem;margin-bottom:6px"><strong>' + m.sender_username + '</strong></div>'; html += '<div>' + escapeHtml(m.message || '') + '</div>'; if (m.attachment) html += '<div><a href="' + m.attachment + '" target="_blank">Anhang öffnen</a></div>'; if (m.flagged) html += '<div style="color:#b91c1c;font-weight:600">Diese Nachricht wurde markiert</div>'; html += '<div style="font-size:12px;color:#8b93a7">' + m.created_at + '</div>'; d.innerHTML = html; w.appendChild(d); }); w.scrollTop = w.scrollHeight; }
        function escapeHtml(s){ return s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
        document.getElementById('sendBtn').onclick = async function(){ const msg = document.getElementById('msgInput').value.trim(); if (!msg && !attachment) return; const res = await api('?', { action: 'send', room: room, msg: msg, attachment: attachment }, 'POST'); if (res.ok){ document.getElementById('msgInput').value = ''; attachment = null; fetchMessages(); sendWsMessage({ type: 'message', room: room, message: msg }); if (res.flagged) alert('Hinweis: Ihre Nachricht wurde automatisch markiert und wird überprüft.'); } else alert(res.msg || 'Fehler'); };
        document.getElementById('attachBtn').onclick = function(){ document.getElementById('fileInput').click(); };
        document.getElementById('fileInput').addEventListener('change', async function(e){ const f = e.target.files[0]; if (!f) return; if (f.size > <?php echo $upload_max_filesize_bytes; ?>) { alert('Datei zu groß'); return; } const fd = new FormData(); fd.append('action','upload'); fd.append('file', f); fd.append('_csrf', csrf); const res = await fetch('?', { method: 'POST', body: fd, credentials: 'include' }); const j = await res.json(); if (j.ok) { attachment = j.file; alert('Upload erfolgreich'); } else alert(j.msg || 'Upload fehlgeschlagen'); });
        document.getElementById('btnNewRoom').onclick = async function(){ const name = prompt('Name des neuen raums'); if (!name) return; const dn = prompt('Anzeigename'); const fd = new FormData(); fd.append('action','create_room'); fd.append('name', name); fd.append('displayname', dn || name); fd.append('_csrf', csrf); const res = await fetch('?', { method: 'POST', body: fd, credentials: 'include' }); const j = await res.json(); if (j.ok) loadRooms(); else alert(j.msg || 'Fehler'); };
        document.getElementById('logoutBtn').onclick = async function(){ const r = await api('?', { action: 'logout' }); if (r.ok) location.reload(); };
        // admin button
        document.getElementById('adminBtn')?.addEventListener('click', async function(){ if (role !== 'admin') return; const main = document.querySelector('.panel.main'); main.innerHTML = '<div class="admin-panel"><h2>Admin Dashboard</h2><div class="stats"><div class="stat-card" id="statUsers">Lade...</div><div class="stat-card" id="statStorage">Lade...</div><div class="stat-card" id="statRooms">Lade...</div></div><div id="adminContent">Lade Berichte...</div></div>'; const s = await api('?', { action: 'admin_stats' }); document.getElementById('statUsers').innerHTML = '<strong>Benutzer</strong><div>' + s.users + '</div>'; document.getElementById('statStorage').innerHTML = '<strong>Speicher</strong><div>' + Math.round(s.storage_bytes/1024) + ' KB</div>'; let roomsHtml = '<strong>Top Räume</strong><ul>'; s.rooms.forEach(r => { roomsHtml += '<li>' + r.room + ': ' + r.cnt + '</li>'; }); roomsHtml += '</ul>'; document.getElementById('statRooms').innerHTML = roomsHtml; const reports = await api('?', { action: 'admin_reports' }); let html = '<h3>Meldungen</h3><table><tr><th>ID</th><th>Nachricht</th><th>Grund</th><th>Reporter</th><th>Aktion</th></tr>'; reports.forEach(r => { html += '<tr><td>' + r.id + '</td><td>' + (r.message ? r.message.substring(0,80) : '') + '</td><td>' + r.reason + '</td><td>' + r.reporter_id + '</td><td><button class="handleBtn" data-id="'+r.id+'">Erledigt</button></td></tr>'; }); html += '</table>'; document.getElementById('adminContent').innerHTML = html; document.querySelectorAll('.handleBtn').forEach(b => b.addEventListener('click', async function(){ const id = this.getAttribute('data-id'); const res = await api('?', { action: 'admin_handle_report', id: id }, 'POST'); if (res.ok) this.parentElement.parentElement.style.opacity = 0.5; else alert('Fehler'); })); });
        
        // websocket client
        let ws = null;
        function connectWs(){
            const proto = location.protocol === 'https:' ? 'wss' : 'ws';
            ws = new WebSocket(proto + '://' + location.hostname + ':9000');
            ws.onopen = function(){ console.log('ws open'); joinWsRoom(); };
            ws.onmessage = function(e){ try { const d = JSON.parse(e.data); if (d.type === 'message') fetchMessages(); } catch(err){} };
            ws.onclose = function(){ setTimeout(connectWs, 3000); };
        }
        function joinWsRoom(){ if (!ws || ws.readyState !== 1) return; ws.send(JSON.stringify({ type: 'join', room: room, username: me })); }
        function sendWsMessage(obj){ if (!ws || ws.readyState !== 1) return; ws.send(JSON.stringify(obj)); }
        connectWs();
        setInterval(function(){ if (room) fetchMessages(); }, 5000);
        loadRooms();
    </script>
    </body>
    </html>
    <?php
    exit;
}

// create room
if ($action === 'create_room'){
    if (!isset($_SESSION['user_id'])){ echo json_encode(['ok' => false, 'msg' => 'Nicht angemeldet']); exit; }
    csrf_check(); $name = clean_username($_POST['name'] ?? ''); $displayname = clean_text($_POST['displayname'] ?? $name, 200); if ($name === ''){ echo json_encode(['ok' => false, 'msg' => 'Name erforderlich']); exit; }
    $mysqli = db(); $stmt = $mysqli->prepare('INSERT IGNORE INTO rooms (name, displayname) VALUES (?, ?)'); $stmt->bind_param('ss', $name, $displayname); $ok = $stmt->execute(); if (!$ok) echo json_encode(['ok' => false, 'msg' => 'Fehler']); else echo json_encode(['ok' => true]); $stmt->close(); $mysqli->close(); exit;
}

// admin delete user endpoint
if ($action === 'admin_delete_user'){
    require_role('admin'); csrf_check(); $id = intval($_POST['id'] ?? 0); if (!$id){ echo json_encode(['ok' => false]); exit; }
    $mysqli = db(); $stmt = $mysqli->prepare('DELETE FROM users WHERE id = ?'); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->close(); $mysqli->close(); echo json_encode(['ok' => true]); exit;
}

http_response_code(404); echo json_encode(['ok' => false, 'msg' => 'Nicht gefunden']);

?>
Dreamcodes Redaktion
Dreamcodes Redaktion
Qualität als Standard. Verantwortung als Prinzip. Jede Ressource auf Dreamcodes basiert auf geprüften Best Practices und fundierter Praxiserfahrung. Unser Anspruch ist ein belastbares Fundament statt experimenteller Lösungen. Die Integration und Absicherung der Inhalte liegt in Ihrem Ermessen. Wir liefern die fachliche Basis, die Verantwortung für den produktiven Einsatz verbleibt bei Ihnen.
Vorheriges Tutorial
Nächstes Tutorial

Vielleicht einen Blick WERT?