OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

entry_form.php

13 KB
<?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') ?>">&times;</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('&#128065; Anzeigen') ?>
                                </button>
                                <button type="button" class="btn btn-outline-secondary" data-act="generatePassword">
                                    <?= $this->te('&#128272; 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 = '&#128584; Verbergen';
    } else {
        inp.type = 'password';
        btn.innerHTML = '&#128065; 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 = '&#128584; 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">&times;</button>'
        + '</div>';
    list.appendChild(div);
    div.querySelector('input').focus();
}
</script>