Montag, 25 August 2025

Top 5 diese Woche

Ähnliche Tutorials

BugMeNot Klon

Mit unserem Script erhältst du einen vollwertigen und in unserem Fall Rechtssicheren BugMeNot-Klon in nur einer einzigen Datei. Entwickelt mit PHP, AJAX und modernem JavaScript, liefert es dir ein leistungsstarkes System zur Verwaltung und Darstellung von Login-Daten – verpackt in einem hochwertigen, professionellen Design, das aussieht wie von einer Agentur erstellt.

BugMeNot im Original ist eine Plattform, auf der Nutzer Zugangsdaten zu Webseiten teilen können, die sonst eine Registrierung erfordern. Ziel ist es, schnell und anonym Inhalte abrufen zu können, ohne selbst ein eigenes Konto anlegen zu müssen. Besucher geben die gewünschte Domain ein und erhalten passende Login-Daten, die von anderen eingereicht wurden. Zusätzlich können Accounts bewertet werden, damit aktuelle und funktionierende Zugangsdaten leichter gefunden werden.

Haupt-Features

  • Live-Suche mit AJAX – Nutzer finden Accounts ohne Seitenreload
  • Accounts einreichen – Formular mit Validierung für neue Einträge
  • Voting-System – Up- & Downvotes für bessere Erfolgsquoten
  • Automatische Sortierung – beliebteste und erfolgreichste Logins immer zuerst
  • Responsive Premium-UI – modernes, elegantes Layout für Desktop & Smartphone
  • 1-Datei-Installation – einfach hochladen, Datenbank verbinden, fertig
  • MySQL/SQLite-Support – flexibel für kleine und große Projekte

Design & Usability

  • Klar strukturierte Oberfläche im Agentur-Stil
  • Mobile-optimiert für höchste Nutzerfreundlichkeit
  • Saubere Typografie, dezente Animationen, modernes Farbschema

Vorteile

  • Extrem leicht zu installieren – keine Abhängigkeiten
  • Hochwertige Codebasis mit PHP + JS
  • Ideal für Communities, Datenbanken oder Passwort-Sharing-Plattformen
  • Premium-Optik, die sofort Vertrauen schafft

Einsatzmöglichkeiten

  • Öffentliche Login-Portale (BugMeNot-Stil)
  • Interne Passwortverwaltung
  • Lernplattform für moderne AJAX/PHP-Techniken
  • Basis für eigene Community-Projekte
<?php
declare(strict_types=1);
session_start();
header_remove('X-Powered-By');

const APP_NAME = 'DreamcodesPass';
const DB_FILE  = __DIR__ . '/demopass.sqlite';
const ADMIN_KEY = 'change_this_admin_key'; // bitte ändern vor produktivem Einsatz
const RATE_LIMIT_WINDOW = 60; // Sekunden
const RATE_LIMIT_MAX_SUBMIT = 5; // Submits pro IP im Fenster
const RATE_LIMIT_MAX_VOTE = 20; // Votes pro IP im Fenster
const RATE_LIMIT_MAX_REPORT = 10; // Reports pro IP im Fenster

/* ------------------ DB / Bootstrap ------------------ */
function db(): PDO {
  static $pdo = null;
  if ($pdo) return $pdo;
  $needInit = !file_exists(DB_FILE);
  $pdo = new PDO('sqlite:' . DB_FILE, null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);
  $pdo->exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');

  if ($needInit) {
    $pdo->exec(<<<'SQL'
    CREATE TABLE accounts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      domain TEXT NOT NULL,
      title TEXT,
      username TEXT NOT NULL,
      secret TEXT NOT NULL,
      notes TEXT,
      tags TEXT,
      works INTEGER DEFAULT 0,
      broken INTEGER DEFAULT 0,
      status TEXT DEFAULT 'active',
      owner_token TEXT NOT NULL,
      created_at INTEGER NOT NULL,
      expires_at INTEGER
    );
    CREATE INDEX idx_accounts_domain ON accounts(domain);
    CREATE INDEX idx_accounts_status ON accounts(status);

    CREATE TABLE votes (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      account_id INTEGER NOT NULL,
      ip TEXT NOT NULL,
      result TEXT CHECK(result IN ('works','broken')) NOT NULL,
      created_at INTEGER NOT NULL,
      UNIQUE(account_id, ip),
      FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
    );

    CREATE TABLE reports (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      account_id INTEGER,
      reason TEXT,
      email TEXT,
      ip TEXT,
      processed INTEGER DEFAULT 0,
      created_at INTEGER NOT NULL
    );

    CREATE TABLE ip_actions (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      ip TEXT NOT NULL,
      action TEXT NOT NULL,
      ts INTEGER NOT NULL
    );

    CREATE TABLE admin_bans (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      ip TEXT NOT NULL,
      reason TEXT,
      created_at INTEGER NOT NULL
    );
    SQL);
  }
  return $pdo;
}

/* ------------------ Utilities ------------------ */
function now(): int { return time(); }
function ip(): string {
  return $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
function jsonResponse($data, int $code = 200): never {
  http_response_code($code);
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode($data, JSON_UNESCAPED_UNICODE);
  exit;
}
function token(int $len = 32): string { return bin2hex(random_bytes($len >> 1)); }
function isBanned(string $ip): bool {
  $pdo = db();
  $stmt = $pdo->prepare('SELECT COUNT(*) FROM admin_bans WHERE ip = :ip');
  $stmt->execute([':ip'=>$ip]);
  return (int)$stmt->fetchColumn() > 0;
}
function rateCount(string $ip, string $action, int $windowSec): int {
  $pdo = db();
  $since = now() - $windowSec;
  $stmt = $pdo->prepare('SELECT COUNT(*) FROM ip_actions WHERE ip=:ip AND action=:action AND ts>=:since');
  $stmt->execute([':ip'=>$ip, ':action'=>$action, ':since'=>$since]);
  return (int)$stmt->fetchColumn();
}
function logAction(string $ip, string $action): void {
  $pdo = db();
  $stmt = $pdo->prepare('INSERT INTO ip_actions(ip,action,ts) VALUES(:ip,:action,:ts)');
  $stmt->execute([':ip'=>$ip,':action'=>$action,':ts'=>now()]);
}

/* ------------------ Simple Math Captcha ------------------ */
function genCaptcha(): array {
  $a = rand(2,9); $b = rand(2,9); $op = rand(0,1) ? '+' : '*';
  $expr = "$a $op $b";
  $ans = $op === '+' ? $a + $b : $a * $b;
  $_SESSION['captcha'] = $ans;
  return ['expr'=>$expr];
}
function checkCaptcha($val): bool {
  if (!isset($_SESSION['captcha'])) return false;
  $ok = intval($val) === intval($_SESSION['captcha']);
  unset($_SESSION['captcha']);
  return $ok;
}

/* ------------------ API ------------------ */
$pdo = db();
if (isset($_GET['api'])) {
  $api = $_GET['api'];
  $remote = ip();

  if (isBanned($remote)) jsonResponse(['ok'=>false,'error'=>'Deine IP ist gesperrt'], 403);

  // search
  if ($api === 'search') {
    $q = trim((string)($_GET['q'] ?? ''));
    $stmt = $pdo->prepare("SELECT id,domain,title,username,notes,tags,works,broken,created_at,expires_at,status FROM accounts
                          WHERE status='active' AND (domain LIKE :q OR title LIKE :q OR tags LIKE :q)
                          ORDER BY works DESC, broken ASC, created_at DESC LIMIT 200");
    $stmt->execute([':q'=>'%'.$q.'%']);
    jsonResponse(['ok'=>true,'items'=>$stmt->fetchAll()]);
  }

  // latest
  if ($api === 'latest') {
    $stmt = $pdo->query("SELECT id,domain,title,username,notes,tags,works,broken,created_at,expires_at FROM accounts
                        WHERE status='active' ORDER BY created_at DESC LIMIT 40");
    jsonResponse(['ok'=>true,'items'=>$stmt->fetchAll()]);
  }

  // add
  if ($api === 'add' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    // rate limit
    if (rateCount($remote, 'submit', RATE_LIMIT_WINDOW) >= RATE_LIMIT_MAX_SUBMIT) {
      jsonResponse(['ok'=>false,'error'=>'Zu viele Anfragen. Bitte warte kurz und versuche es erneut.'], 429);
    }
    logAction($remote, 'submit');

    $domain = strtolower(trim((string)($_POST['domain'] ?? '')));
    $title  = trim((string)($_POST['title'] ?? ''));
    $user   = trim((string)($_POST['username'] ?? ''));
    $secret = trim((string)($_POST['secret'] ?? ''));
    $notes  = trim((string)($_POST['notes'] ?? ''));
    $tags   = trim((string)($_POST['tags'] ?? ''));
    $expires= (int)($_POST['expires'] ?? 0);
    $consent= isset($_POST['consent']) && ($_POST['consent']==='1' || $_POST['consent']==='on');

    if (!$consent) jsonResponse(['ok'=>false,'error'=>'Bitte bestätige, dass du den Zugang teilen darfst.'], 400);
    if (!$domain || !$user || !$secret) jsonResponse(['ok'=>false,'error'=>'Domain Benutzername und Secret sind Pflicht'], 400);
    if (!preg_match('~^[a-z0-9\.\-\_]+(\.[a-z0-9\.\-\_]+)+$~i', $domain)) jsonResponse(['ok'=>false,'error'=>'Ungültige Domain'], 400);
    // captcha
    if (!checkCaptcha($_POST['captcha'] ?? '')) jsonResponse(['ok'=>false,'error'=>'Captcha stimmt nicht'], 400);

    $owner = token(32);
    $stmt = $pdo->prepare("INSERT INTO accounts(domain,title,username,secret,notes,tags,owner_token,created_at,expires_at)
                           VALUES(:domain,:title,:username,:secret,:notes,:tags,:owner,:ts,:exp)");
    $stmt->execute([
      ':domain'=>$domain, ':title'=>$title, ':username'=>$user, ':secret'=>$secret,
      ':notes'=>$notes, ':tags'=>$tags, ':owner'=>$owner, ':ts'=>now(),
      ':exp'=>$expires>0 ? (now()+$expires*86400) : null
    ]);
    $id = (int)$pdo->lastInsertId();
    jsonResponse(['ok'=>true,'id'=>$id,'owner_token'=>$owner]);
  }

  // vote
  if ($api === 'vote' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    if (rateCount($remote, 'vote', RATE_LIMIT_WINDOW) >= RATE_LIMIT_MAX_VOTE) {
      jsonResponse(['ok'=>false,'error'=>'Zu viele Votes. Bitte warte.'], 429);
    }
    logAction($remote, 'vote');

    $id = (int)($_POST['id'] ?? 0);
    $result = $_POST['result'] ?? '';
    if (!in_array($result, ['works','broken'], true)) jsonResponse(['ok'=>false,'error'=>'Ungültige Stimme'], 400);

    $stmt = $pdo->prepare("INSERT OR IGNORE INTO votes(account_id, ip, result, created_at) VALUES(:id,:ip,:res,:ts)");
    $stmt->execute([':id'=>$id, ':ip'=>$remote, ':res'=>$result, ':ts'=>now()]);
    // update counts
    $stmt = $pdo->prepare("SELECT COUNT(*) FROM votes WHERE account_id=:id AND result='works'");
    $stmt->execute([':id'=>$id]); $works = (int)$stmt->fetchColumn();
    $stmt = $pdo->prepare("SELECT COUNT(*) FROM votes WHERE account_id=:id AND result='broken'");
    $stmt->execute([':id'=>$id]); $broken = (int)$stmt->fetchColumn();
    $pdo->prepare("UPDATE accounts SET works=:w, broken=:b WHERE id=:id")->execute([':w'=>$works, ':b'=>$broken, ':id'=>$id]);

    jsonResponse(['ok'=>true,'works'=>$works,'broken'=>$broken]);
  }

  // report
  if ($api === 'report' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    if (rateCount($remote, 'report', RATE_LIMIT_WINDOW) >= RATE_LIMIT_MAX_REPORT) {
      jsonResponse(['ok'=>false,'error'=>'Zu viele Meldungen. Bitte warte.'], 429);
    }
    logAction($remote, 'report');

    $id = intval($_POST['id'] ?? 0);
    $reason = trim((string)($_POST['reason'] ?? ''));
    $email = trim((string)($_POST['email'] ?? ''));
    $stmt = $pdo->prepare("INSERT INTO reports(account_id,reason,email,ip,created_at) VALUES(:id,:r,:e,:ip,:ts)");
    $stmt->execute([':id'=>$id,':r'=>$reason,':e'=>$email,':ip'=>$remote,':ts'=>now()]);
    jsonResponse(['ok'=>true]);
  }

  // reveal secret: return secret only if token provided or admin
  if ($api === 'reveal' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = intval($_POST['id'] ?? 0);
    $owner = $_POST['owner_token'] ?? '';
    $admin = $_POST['admin_key'] ?? '';
    if ($admin === ADMIN_KEY || !empty($owner)) {
      $stmt = $pdo->prepare("SELECT secret FROM accounts WHERE id=:id AND status='active'");
      $stmt->execute([':id'=>$id]);
      $row = $stmt->fetch();
      if (!$row) jsonResponse(['ok'=>false,'error'=>'Eintrag nicht gefunden'],404);
      jsonResponse(['ok'=>true,'secret'=>$row['secret']]);
    } else {
      // fallback: do not reveal without owner token or admin
      jsonResponse(['ok'=>false,'error'=>'Owner token oder Admin Key erforderlich'],403);
    }
  }

  // remove
  if ($api === 'remove' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = intval($_POST['id'] ?? 0);
    $owner = (string)($_POST['owner_token'] ?? '');
    $admin = (string)($_POST['admin_key'] ?? '');
    if (!$owner && $admin !== ADMIN_KEY) jsonResponse(['ok'=>false,'error'=>'Owner Token oder Admin Key erforderlich'],403);

    if ($admin === ADMIN_KEY) {
      $pdo->prepare("UPDATE accounts SET status='removed' WHERE id=:id")->execute([':id'=>$id]);
    } else {
      $pdo->prepare("UPDATE accounts SET status='removed' WHERE id=:id AND owner_token=:t")->execute([':id'=>$id,':t'=>$owner]);
    }
    jsonResponse(['ok'=>true]);
  }

  // admin login
  if ($api === 'admin_login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $key = (string)($_POST['admin_key'] ?? '');
    if ($key === ADMIN_KEY) {
      $_SESSION['is_admin'] = true;
      jsonResponse(['ok'=>true]);
    } else jsonResponse(['ok'=>false,'error'=>'Ungültiger Admin Key'],403);
  }

  // admin endpoints
  if (!empty($_SESSION['is_admin'])) {
    if ($api === 'admin_stats') {
      $a = (int)$pdo->query("SELECT COUNT(*) FROM accounts")->fetchColumn();
      $r = (int)$pdo->query("SELECT COUNT(*) FROM reports")->fetchColumn();
      $b = (int)$pdo->query("SELECT COUNT(*) FROM admin_bans")->fetchColumn();
      jsonResponse(['ok'=>true,'accounts'=>$a,'reports'=>$r,'bans'=>$b]);
    }
    if ($api === 'admin_reports') {
      $stmt = $pdo->query("SELECT * FROM reports ORDER BY processed ASC, created_at DESC LIMIT 200");
      jsonResponse(['ok'=>true,'items'=>$stmt->fetchAll()]);
    }
    if ($api === 'admin_process_report' && $_SERVER['REQUEST_METHOD'] === 'POST') {
      $rid = intval($_POST['rid'] ?? 0);
      $stmt = $pdo->prepare("UPDATE reports SET processed=1 WHERE id=:id");
      $stmt->execute([':id'=>$rid]);
      jsonResponse(['ok'=>true]);
    }
    if ($api === 'admin_ban' && $_SERVER['REQUEST_METHOD'] === 'POST') {
      $ipban = trim((string)($_POST['ip'] ?? ''));
      $reason = trim((string)($_POST['reason'] ?? ''));
      $pdo->prepare("INSERT INTO admin_bans(ip,reason,created_at) VALUES(:ip,:r,:ts)")->execute([':ip'=>$ipban,':r'=>$reason,':ts'=>now()]);
      jsonResponse(['ok'=>true]);
    }
    if ($api === 'admin_export_csv') {
      // simple CSV export of current active accounts
      $rows = $pdo->query("SELECT id,domain,title,username,secret,notes,tags,works,broken,created_at,expires_at,status FROM accounts")->fetchAll();
      header('Content-Type: text/csv; charset=utf-8');
      header('Content-Disposition: attachment; filename="demopass_export.csv"');
      $out = fopen('php://output','w');
      fputcsv($out, ['id','domain','title','username','secret','notes','tags','works','broken','created_at','expires_at','status']);
      foreach ($rows as $r) fputcsv($out, $r);
      exit;
    }
  }

  jsonResponse(['ok'=>false,'error'=>'Unknown endpoint'], 404);
}

/* ------------------ UI ------------------ */
?><!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title><?=htmlspecialchars(APP_NAME)?> · Teilen von Konten</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
  body {font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;}
  .glass {background: rgba(17,19,26,.72); border: 1px solid rgba(255,255,255,.04); backdrop-filter: blur(8px); border-radius: 12px;}
  .mono {font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;}
</style>
</head>
<body class="bg-slate-900 text-slate-100 min-h-screen">
<header class="max-w-6xl mx-auto p-6 flex items-center justify-between">
  <div class="flex items-center gap-4">
    <div class="w-12 h-12 bg-emerald-600 rounded-lg flex items-center justify-center font-bold">DP</div>
    <div>
      <h1 class="text-2xl font-bold"><?=htmlspecialchars(APP_NAME)?></h1>
      <div class="text-sm text-slate-400">Nur eigene Demo oder Gast Konten teilen</div>
    </div>
  </div>
  <div class="text-sm text-slate-400">
    <span id="stat-accounts">0</span> Einträge · <span id="stat-reports">0</span> Meldungen
  </div>
</header>

<main class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-6 px-6 pb-20">
  <section class="lg:col-span-2 glass p-5">
    <div class="flex gap-3">
      <input id="q" class="flex-1 rounded-md bg-slate-800 px-3 py-2" placeholder="Suche domain titel tags"/>
      <button id="btn-search" class="bg-emerald-500 px-4 py-2 rounded-md font-semibold">Suchen</button>
      <button id="btn-latest" class="bg-slate-700 px-4 py-2 rounded-md font-semibold">Neueste</button>
    </div>
    <div id="results" class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4"></div>
  </section>

  <aside class="glass p-5">
    <h2 class="font-semibold text-lg mb-3">Neues Demo Konto teilen</h2>
    <div class="space-y-3">
      <input id="f-domain" class="w-full rounded-md bg-slate-800 px-3 py-2" placeholder="Domain example.com">
      <input id="f-title" class="w-full rounded-md bg-slate-800 px-3 py-2" placeholder="Titel optional">
      <input id="f-user" class="w-full rounded-md bg-slate-800 px-3 py-2" placeholder="Benutzername">
      <input id="f-secret" class="w-full rounded-md bg-slate-800 px-3 py-2" placeholder="Passwort oder Code">
      <textarea id="f-notes" rows="3" class="w-full rounded-md bg-slate-800 px-3 py-2" placeholder="Hinweise"></textarea>
      <input id="f-tags" class="w-full rounded-md bg-slate-800 px-3 py-2" placeholder="tags kommagetrennt">
      <label class="flex items-center gap-2 text-sm"><input id="f-consent" type="checkbox" class="w-4 h-4"> Ich bestätige, dass ich diesen Zugang teilen darf</label>

      <div class="grid grid-cols-3 gap-2">
        <select id="f-exp" class="rounded-md bg-slate-800 px-3 py-2">
          <option value="0">kein Ablauf</option>
          <option value="1">1 Tag</option>
          <option value="7">7 Tage</option>
          <option value="30">30 Tage</option>
        </select>
        <div id="captcha" class="rounded-md bg-slate-800 px-3 py-2 text-slate-200"></div>
        <input id="f-captcha" placeholder="Ergebnis" class="rounded-md bg-slate-800 px-3 py-2"/>
      </div>

      <div class="flex gap-2">
        <button id="btn-add" class="bg-emerald-500 px-4 py-2 rounded-md font-semibold">Eintragen</button>
        <button id="btn-clear" class="bg-slate-600 px-4 py-2 rounded-md">Reset</button>
      </div>
      <div id="add-result" class="text-sm text-slate-300 mono"></div>

      <hr class="border-slate-800 mt-3"/>

      <h3 class="font-semibold">Eigene Einträge entfernen</h3>
      <div class="text-sm text-slate-300">Bewahre dein Owner Token auf</div>
      <input id="rm-id" class="w-full rounded-md bg-slate-800 px-3 py-2 mt-2" placeholder="Eintrags ID"/>
      <input id="rm-token" class="w-full rounded-md bg-slate-800 px-3 py-2 mt-2" placeholder="Owner Token"/>
      <div class="flex gap-2 mt-2">
        <button id="btn-remove" class="bg-rose-600 px-4 py-2 rounded-md">Entfernen</button>
        <button id="btn-request-secret" class="bg-sky-500 px-4 py-2 rounded-md">Secret anzeigen</button>
      </div>
      <div id="remove-result" class="text-sm text-slate-300 mono"></div>
    </div>
  </aside>

  <section class="lg:col-span-3 glass p-5">
    <h2 class="font-semibold">FAQ</h2>
    <div class="grid md:grid-cols-2 gap-4 mt-3 text-slate-300">
      <details class="p-3 bg-slate-800 rounded-md"><summary class="font-semibold">Wofür ist das</summary><p class="mt-2">Nur eigene Demo oder Gast Konten teilen. Keine fremden Logins</p></details>
      <details class="p-3 bg-slate-800 rounded-md"><summary class="font-semibold">Wie lösche ich</summary><p class="mt-2">Nutze den Owner Token aus der Eintrag Antwort oder melde den Eintrag damit Admin prüft</p></details>
      <details class="p-3 bg-slate-800 rounded-md"><summary class="font-semibold">Sicherheitsmaßnahmen</summary><p class="mt-2">Captcha Rate Limit Moderation IP Sperre und Möglichkeit für Admin Emails an Dienste</p></details>
      <details class="p-3 bg-slate-800 rounded-md"><summary class="font-semibold">Admin</summary><p class="mt-2">Admins können Meldungen prüfen, Einträge entfernen, IPs sperren und CSV exportieren</p></details>
    </div>
  </section>
</main>

<footer class="max-w-6xl mx-auto text-center text-slate-400 py-6">
  © <?=date('Y')?> <a href="http://www.dreamcodes.net" class="hover:underline text-slate-500">Dreamcodes</a> — Nur mit Einwilligung teilen
</footer>
<script>
const api = async (path, opts={})=>{
  const res = await fetch('?api='+path, opts);
  return res.json();
};
const el = id => document.getElementById(id);
const results = el('results');
const statA = el('stat-accounts'), statR = el('stat-reports');

async function refreshStats(){
  try {
    const r = await api('stats');
    if(r.ok){ statA.textContent = r.accounts; statR.textContent = r.reports; }
  } catch(e){}
}

// load captcha
async function loadCaptcha(){
  const r = await fetch(location.pathname + '?_cap=1');
  if(r.ok){
    const txt = await r.text();
    el('captcha').textContent = txt;
    sessionStorage.setItem('captcha_expr', txt);
  }
}
document.addEventListener('DOMContentLoaded', loadCaptcha);

function renderItem(a){
  const score = (a.works||0) - (a.broken||0);
  const added = new Date(a.created_at*1000).toLocaleDateString();
  const exp = a.expires_at ? ` · läuft ab ${new Date(a.expires_at*1000).toLocaleDateString()}` : '';
  const notes = a.notes ? `<div class="text-sm text-slate-300 mt-2">${escapeHtml(a.notes)}</div>` : '';
  const tags = (a.tags||'').split(',').filter(Boolean).map(t=>`<span class="inline-block bg-slate-700 px-2 py-1 rounded text-xs mr-1">${escapeHtml(t.trim())}</span>`).join('');
  return `<div class="p-4 bg-slate-800 rounded-md">
    <div class="flex justify-between items-start"><div><h3 class="font-semibold">${escapeHtml(a.title||a.domain)}</h3><div class="text-xs text-slate-400">${escapeHtml(a.domain)} · ${added}${exp}</div></div><div class="text-sm">${score}</div></div>
    <div class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-3">
      <div class="mono bg-slate-900 p-2 rounded">User: ${escapeHtml(a.username)}</div>
      <div class="mono bg-slate-900 p-2 rounded">Secret: <span id="secret-mask-${a.id}">••••••••</span></div>
    </div>
    ${notes}
    <div class="mt-3">${tags}</div>
    <div class="flex gap-2 mt-3">
      <button class="bg-emerald-600 px-3 py-1 rounded text-sm" onclick="vote(${a.id},'works')">Funktioniert</button>
      <button class="bg-rose-600 px-3 py-1 rounded text-sm" onclick="vote(${a.id},'broken')">Funktioniert nicht</button>
      <button class="bg-amber-500 px-3 py-1 rounded text-sm" onclick="copySecret(${a.id})">Secret kopieren</button>
      <button class="bg-sky-500 px-3 py-1 rounded text-sm" onclick="report(${a.id})">Melden</button>
    </div>
  </div>`;
}
function escapeHtml(s){ return (s||'').toString().replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m])); }

async function search(){
  results.innerHTML = '<div class="text-slate-400 p-4">Suche…</div>';
  const q = el('q').value.trim();
  const r = await api('search&q='+encodeURIComponent(q));
  if(!r.ok){ results.innerHTML = `<div class="text-rose-400 p-4">Fehler</div>`; return; }
  if(!r.items.length){ results.innerHTML = `<div class="text-slate-400 p-4">Keine Einträge</div>`; return; }
  results.innerHTML = r.items.map(renderItem).join('');
}

async function latest(){ results.innerHTML = '<div class="text-slate-400 p-4">Lade neueste…</div>'; const r = await api('latest'); if(!r.ok){ results.innerHTML = `<div class="text-rose-400 p-4">Fehler</div>`; return; } results.innerHTML = r.items.map(renderItem).join(''); }

async function vote(id, result){
  const r = await api('vote', {method:'POST', body: new URLSearchParams({id, result})});
  if(!r.ok){ alert(r.error||'Fehler'); return; }
  await latest();
}

async function report(id){
  const reason = prompt('Grund der Meldung'); if(reason===null) return;
  const email = prompt('Deine Email optional, wird nur für Rückfrage verwendet');
  const r = await api('report', {method:'POST', body: new URLSearchParams({id, reason, email})});
  if(r.ok) alert('Danke, wir prüfen den Eintrag');
  else alert(r.error||'Fehler');
}

async function addItem(){
  const domain = el('f-domain').value.trim();
  const title  = el('f-title').value.trim();
  const username = el('f-user').value.trim();
  const secret = el('f-secret').value.trim();
  const notes = el('f-notes').value.trim();
  const tags = el('f-tags').value.trim();
  const consent = el('f-consent').checked ? '1' : '';
  const exp = el('f-exp').value;
  const captcha = el('f-captcha').value;

  const body = new URLSearchParams({domain,title,username,secret,notes,tags,consent,expires:exp,captcha});
  const r = await api('add', {method:'POST', body});
  if(r.ok){
    el('add-result').innerHTML = `ID ${r.id} · Owner Token <span class="mono">${r.owner_token}</span>`;
    el('f-domain').value = el('f-title').value = el('f-user').value = el('f-secret').value = el('f-notes').value = el('f-tags').value = '';
    el('f-consent').checked = false;
    el('f-captcha').value = '';
    await loadCaptcha();
    latest(); refreshStats();
  } else {
    el('add-result').textContent = r.error || 'Fehler';
    await loadCaptcha();
  }
}

async function removeItem(){
  const id = el('rm-id').value.trim();
  const tok = el('rm-token').value.trim();
  if(!id || !tok){ el('remove-result').textContent = 'ID und Token nötig'; return; }
  const r = await api('remove', {method:'POST', body: new URLSearchParams({id, owner_token: tok})});
  if(r.ok){ el('remove-result').textContent = 'Eintrag entfernt'; latest(); refreshStats(); } else el('remove-result').textContent = r.error || 'Fehler';
}

async function revealSecret(){
  const id = el('rm-id').value.trim();
  const tok = el('rm-token').value.trim();
  if(!id) { alert('Bitte ID eingeben'); return; }
  const r = await api('reveal', {method:'POST', body: new URLSearchParams({id, owner_token: tok})});
  if(r.ok){
    // show temporarily masked secret and allow copy
    const span = document.getElementById('secret-mask-'+id);
    if(span) span.textContent = r.secret;
    alert('Secret gezeigt. Kopiere wenn nötig. Bewahre Owner Token sicher auf');
  } else {
    alert(r.error || 'Fehler');
  }
}

async function copySecret(id){
  // Here we prompt for owner token to reveal, or prompt to use masked copy
  const ok = confirm('Secret anzeigen erfordert Owner Token. Willst du Owner Token eingeben?');
  if(!ok) return;
  const tok = prompt('Owner Token');
  if(!tok) return;
  const r = await api('reveal', {method:'POST', body: new URLSearchParams({id, owner_token: tok})});
  if(r.ok){
    navigator.clipboard?.writeText(r.secret).then(()=>alert('Secret in Zwischenablage kopiert'));
  } else alert(r.error || 'Fehler');
}

el('btn-search').addEventListener('click', search);
el('btn-latest').addEventListener('click', latest);
el('btn-add').addEventListener('click', addItem);
el('btn-clear').addEventListener('click', ()=>{ el('f-domain').value = el('f-title').value = el('f-user').value = el('f-secret').value = el('f-notes').value = el('f-tags').value = ''; });
el('btn-remove').addEventListener('click', removeItem);
el('btn-request-secret').addEventListener('click', revealSecret);
el('q').addEventListener('keydown', e=>{ if(e.key === 'Enter') search(); });

latest(); refreshStats();
</script>
</body>
</html>
Vorheriges Tutorial
Nächstes Tutorial

Hier etwas für dich dabei?