Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<div class="content-header">
<div>
<h1><?= $this->e($page_title) ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="<?= $this->e($back_url ?? '/vault') ?>"><?= $this->te('Passwörter') ?></a></li>
<li class="breadcrumb-item active"><?= $this->e($page_title) ?></li>
</ol>
</div>
</div>
<div class="row">
<div class="col-lg-7 col-md-9">
<div class="card card-primary card-outline">
<div class="card-body">
<form method="POST" action="<?= $this->e($form_action) ?>">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<?php if ($entry !== null): ?>
<input type="hidden" name="_method" value="PUT">
<?php endif; ?>
<!-- Titel -->
<div class="form-group">
<label for="title"><?= $this->te('Titel') ?> <span class="text-danger">*</span></label>
<input type="text" id="title" name="title" class="form-control"
value="<?= $this->e($entry['title'] ?? '') ?>" required autofocus
placeholder="<?= $this->te('z.B. GitHub') ?>">
</div>
<!-- URLs -->
<div class="form-group">
<label><?= $this->te('URL(s)') ?></label>
<div id="urlList">
<?php
$existingUrls = [];
if (!empty($entry['url'])) {
foreach (explode("\n", $entry['url']) as $u) {
$u = trim($u);
if ($u !== '') $existingUrls[] = $u;
}
}
if (empty($existingUrls)) $existingUrls = [''];
foreach ($existingUrls as $i => $urlVal):
?>
<div class="input-group mb-1" id="urlRow<?= $i ?>">
<input type="url" name="urls[]" class="form-control"
value="<?= $this->e($urlVal) ?>"
placeholder="https://example.com">
<?php if ($i > 0): ?>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
data-remove-closest=".input-group" title="<?= $this->te('Entfernen') ?>">×</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn btn-link btn-sm p-0" data-act="addUrlRow">
<?= $this->te('+ Weitere URL hinzufügen') ?>
</button>
</div>
<!-- Benutzername -->
<div class="form-group">
<label for="username"><?= $this->te('Benutzername / E-Mail') ?></label>
<input type="text" id="username" name="username" class="form-control"
value="<?= $this->e($entry['username'] ?? '') ?>"
placeholder="user@example.com" autocomplete="off">
</div>
<!-- Passwort -->
<div class="form-group">
<label for="password"><?= $this->te('Passwort') ?> <span class="text-danger">*</span></label>
<div class="input-group">
<input type="password" id="password" name="password" class="form-control"
value="<?= $this->e($entry['password'] ?? '') ?>"
required autocomplete="new-password">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" data-act="togglePw" id="pwToggleBtn">
<?= $this->te('👁 Anzeigen') ?>
</button>
<button type="button" class="btn btn-outline-secondary" data-act="generatePassword">
<?= $this->te('🔐 Gen.') ?>
</button>
</div>
</div>
<!-- Generator-Optionen -->
<div class="mt-1" style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;font-size:0.82rem;">
<span class="text-muted"><?= $this->te('Länge:') ?></span>
<input type="number" id="genLength" value="20" min="8" max="128"
style="width:58px;padding:0.2rem 0.4rem;border:1px solid var(--color-gray-400);border-radius:0.25rem;font-size:0.82rem;">
<label style="margin:0;display:flex;align-items:center;gap:0.25rem;cursor:pointer;">
<input type="checkbox" id="genUpper" checked> A–Z
</label>
<label style="margin:0;display:flex;align-items:center;gap:0.25rem;cursor:pointer;">
<input type="checkbox" id="genNumbers" checked> 0–9
</label>
<label style="margin:0;display:flex;align-items:center;gap:0.25rem;cursor:pointer;">
<input type="checkbox" id="genSymbols" checked> !@#$
</label>
</div>
<!-- Stärke-Anzeige -->
<div class="vps-wrap" style="margin-top:0.5rem;">
<div style="height:5px;background:var(--color-gray-100);border-radius:3px;overflow:hidden;">
<div id="vpsBar" style="height:100%;width:0;background:var(--red-600);transition:width .2s,background .2s;"></div>
</div>
<div id="vpsLabel" style="font-size:0.74rem;color:var(--gray-400);margin-top:0.25rem;"><?= $this->te('Passwortstärke') ?></div>
</div>
</div>
<!-- TOTP -->
<div class="form-group">
<label for="totp"><?= $this->te('TOTP-Secret') ?> <span class="text-muted"><?= $this->te('(optional)') ?></span></label>
<input type="text" id="totp" name="totp" class="form-control"
value="<?= $this->e($entry['totp'] ?? '') ?>"
placeholder="<?= $this->te('Base32-kodiertes Secret, z.B. JBSWY3DPEHPK3PXP') ?>"
autocomplete="off" spellcheck="false">
<small class="form-text text-muted">
<?= $this->te('Das Secret aus dem QR-Code Ihres Authenticators (nicht den 6-stelligen Code, sondern den Einrichtungsschlüssel).') ?>
</small>
</div>
<!-- Ordner -->
<div class="form-group">
<label for="folder_id"><?= $this->te('Ordner') ?></label>
<select id="folder_id" name="folder_id" class="form-control">
<option value=""><?= $this->te('— Kein Ordner —') ?></option>
<?php
$selectedFolderId = $entry['folder_id'] ?? $preselect_folder_id ?? null;
foreach ($folders as $folder): ?>
<option value="<?= $folder['id'] ?>"
<?= $selectedFolderId == $folder['id'] ? 'selected' : '' ?>>
<?= $this->e($folder['path_label'] ?? $folder['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Notizen -->
<div class="form-group">
<label for="notes"><?= $this->te('Notizen') ?></label>
<textarea id="notes" name="notes" class="form-control" rows="3"
placeholder="<?= $this->te('Optionale Anmerkungen...') ?>"><?= $this->e($entry['notes'] ?? '') ?></textarea>
</div>
<div class="form-group mb-0">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> <?= $this->te('Speichern') ?>
</button>
<a href="<?= $this->e($back_url ?? '/vault') ?>" class="btn btn-default ml-2"><?= $this->te('Abbrechen') ?></a>
</div>
</form>
</div>
</div>
</div>
</div>
<script<?= $this->scriptNonce() ?>>
function togglePw() {
const inp = document.getElementById('password');
const btn = document.getElementById('pwToggleBtn');
if (inp.type === 'password') {
inp.type = 'text';
btn.innerHTML = '🙈 Verbergen';
} else {
inp.type = 'password';
btn.innerHTML = '👁 Anzeigen';
}
}
function generatePassword() {
const len = parseInt(document.getElementById('genLength').value) || 20;
const upper = document.getElementById('genUpper').checked;
const numbers = document.getElementById('genNumbers').checked;
const symbols = document.getElementById('genSymbols').checked;
let chars = 'abcdefghijklmnopqrstuvwxyz';
if (upper) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (numbers) chars += '0123456789';
if (symbols) chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';
const arr = new Uint32Array(len);
crypto.getRandomValues(arr);
let pw = '';
for (let i = 0; i < len; i++) pw += chars[arr[i] % chars.length];
const inp = document.getElementById('password');
inp.type = 'text';
inp.value = pw;
document.getElementById('pwToggleBtn').innerHTML = '🙈 Verbergen';
updateStrength();
}
// ── Passwortstärke (leichtgewichtige Entropie-Schätzung) ──────────────
function updateStrength() {
const pw = document.getElementById('password').value || '';
const bar = document.getElementById('vpsBar');
const lbl = document.getElementById('vpsLabel');
if (!bar) return;
let pool = 0;
if (/[a-z]/.test(pw)) pool += 26;
if (/[A-Z]/.test(pw)) pool += 26;
if (/[0-9]/.test(pw)) pool += 10;
if (/[^a-zA-Z0-9]/.test(pw)) pool += 32;
const entropy = pw.length ? Math.round(pw.length * Math.log2(pool || 1)) : 0;
// Stufen: <40 schwach, <70 ok, <100 stark, sonst sehr stark
let pct, color, text;
if (!pw) { pct = 0; color = '#edeff4'; text = 'Passwortstärke'; }
else if (entropy < 40) { pct = 25; color = '#dc2626'; text = 'Schwach (' + entropy + ' bit)'; }
else if (entropy < 70) { pct = 55; color = '#f59e0b'; text = 'Mittel (' + entropy + ' bit)'; }
else if (entropy < 100) { pct = 80; color = '#16a34a'; text = 'Stark (' + entropy + ' bit)'; }
else { pct = 100; color = '#15803d'; text = 'Sehr stark (' + entropy + ' bit)'; }
bar.style.width = pct + '%';
bar.style.background = color;
lbl.textContent = text;
lbl.style.color = pw ? color : '#9ca3af';
}
document.addEventListener('DOMContentLoaded', function () {
const inp = document.getElementById('password');
if (inp) { inp.addEventListener('input', updateStrength); updateStrength(); }
});
let urlRowCount = document.querySelectorAll('#urlList .input-group').length;
function addUrlRow() {
const list = document.getElementById('urlList');
const div = document.createElement('div');
div.className = 'input-group mb-1';
div.id = 'urlRow' + urlRowCount++;
div.innerHTML = '<input type="url" name="urls[]" class="form-control" placeholder="https://example.com">'
+ '<div class="input-group-append">'
+ '<button type="button" class="btn btn-outline-secondary" data-remove-closest=".input-group" title="Entfernen">×</button>'
+ '</div>';
list.appendChild(div);
div.querySelector('input').focus();
}
</script>