Remover bloqueio do cofre de senhas
This commit is contained in:
199
assets/app.js
199
assets/app.js
@@ -4,7 +4,6 @@
|
|||||||
const STORAGE = {
|
const STORAGE = {
|
||||||
UNITS: "telseg_units",
|
UNITS: "telseg_units",
|
||||||
CREDS: "telseg_passwords",
|
CREDS: "telseg_passwords",
|
||||||
VAULT: "telseg_vault",
|
|
||||||
THEME: "telseg_theme",
|
THEME: "telseg_theme",
|
||||||
NOTES: "telseg_notes",
|
NOTES: "telseg_notes",
|
||||||
AUTH: "telseg_auth",
|
AUTH: "telseg_auth",
|
||||||
@@ -14,7 +13,7 @@ const $ = (sel, el = document) => el.querySelector(sel);
|
|||||||
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
|
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
|
||||||
|
|
||||||
// Detecção de API
|
// Detecção de API
|
||||||
const state = { useApi: false, units: [], creds: [], vault: null, notes: [], auth: null };
|
const state = { useApi: false, units: [], creds: [], notes: [], auth: null };
|
||||||
|
|
||||||
async function probeApi() {
|
async function probeApi() {
|
||||||
try {
|
try {
|
||||||
@@ -264,17 +263,6 @@ function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&",
|
|||||||
function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "'"); }
|
function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "'"); }
|
||||||
function formatDate(s) { try { const [y,m,d] = (s||"").split("-"); return d && m && y ? `${d}/${m}/${y}` : s; } catch { return s; } }
|
function formatDate(s) { try { const [y,m,d] = (s||"").split("-"); return d && m && y ? `${d}/${m}/${y}` : s; } catch { return s; } }
|
||||||
|
|
||||||
// WebCrypto guard (requer HTTPS/localhost)
|
|
||||||
const CRYPTO_REQUIRE_MSG = "O cofre precisa de HTTPS (ou localhost) porque o navegador bloqueia a criptografia em origens inseguras.";
|
|
||||||
let cryptoWarned = false;
|
|
||||||
function hasCrypto() { return !!(globalThis.crypto && globalThis.crypto.subtle); }
|
|
||||||
function ensureCrypto(msgTarget) {
|
|
||||||
if (hasCrypto()) return true;
|
|
||||||
if (msgTarget) msgTarget.textContent = CRYPTO_REQUIRE_MSG;
|
|
||||||
if (!cryptoWarned) { alert(CRYPTO_REQUIRE_MSG); cryptoWarned = true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// ------- Cofre de Senhas (WebCrypto) -------
|
|
||||||
async function refreshCreds() {
|
async function refreshCreds() {
|
||||||
if (state.useApi) {
|
if (state.useApi) {
|
||||||
const r = await apiFetch('/api/creds');
|
const r = await apiFetch('/api/creds');
|
||||||
@@ -286,18 +274,7 @@ async function refreshCreds() {
|
|||||||
}
|
}
|
||||||
function loadCreds() { return state.creds || []; }
|
function loadCreds() { return state.creds || []; }
|
||||||
function saveCreds(list) { saveJSON(STORAGE.CREDS, list); state.creds = list; }
|
function saveCreds(list) { saveJSON(STORAGE.CREDS, list); state.creds = list; }
|
||||||
|
function getCredPassword(cred) { return cred?.passwordEnc ?? ""; }
|
||||||
async function refreshVault() {
|
|
||||||
if (state.useApi) {
|
|
||||||
const r = await apiFetch('/api/vault');
|
|
||||||
state.vault = r.ok ? await r.json() : null;
|
|
||||||
} else {
|
|
||||||
state.vault = loadJSON(STORAGE.VAULT, null);
|
|
||||||
}
|
|
||||||
return state.vault;
|
|
||||||
}
|
|
||||||
function getVault() { return state.vault; }
|
|
||||||
function setVault(v) { saveJSON(STORAGE.VAULT, v); state.vault = v; }
|
|
||||||
|
|
||||||
// ------- Notas -------
|
// ------- Notas -------
|
||||||
async function refreshNotes() {
|
async function refreshNotes() {
|
||||||
@@ -417,97 +394,6 @@ function openNoteView(n) {
|
|||||||
showModal('noteViewModal');
|
showModal('noteViewModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
const textEnc = new TextEncoder();
|
|
||||||
const textDec = new TextDecoder();
|
|
||||||
|
|
||||||
function ab2b64(ab) {
|
|
||||||
const bytes = new Uint8Array(ab);
|
|
||||||
let bin = ""; for (let i=0; i<bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
||||||
return btoa(bin);
|
|
||||||
}
|
|
||||||
function b642ab(b64) {
|
|
||||||
const bin = atob(b64); const len = bin.length; const bytes = new Uint8Array(len);
|
|
||||||
for (let i=0; i<len; i++) bytes[i] = bin.charCodeAt(i);
|
|
||||||
return bytes.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deriveKey(password, saltB64, iterations) {
|
|
||||||
if (!hasCrypto()) throw new Error('crypto_unavailable');
|
|
||||||
const salt = b642ab(saltB64);
|
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
|
||||||
"raw", textEnc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]
|
|
||||||
);
|
|
||||||
return crypto.subtle.deriveKey(
|
|
||||||
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
|
|
||||||
keyMaterial,
|
|
||||||
{ name: "AES-GCM", length: 256 },
|
|
||||||
false,
|
|
||||||
["encrypt", "decrypt"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function encryptString(key, plain) {
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, textEnc.encode(plain));
|
|
||||||
return { iv: ab2b64(iv), data: ab2b64(ct) };
|
|
||||||
}
|
|
||||||
async function decryptString(key, enc) {
|
|
||||||
const iv = new Uint8Array(b642ab(enc.iv));
|
|
||||||
const data = b642ab(enc.data);
|
|
||||||
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
|
|
||||||
return textDec.decode(pt);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vaultKey = null;
|
|
||||||
const VAULT_ITERS = 200_000;
|
|
||||||
|
|
||||||
function renderVaultGate() {
|
|
||||||
const meta = getVault();
|
|
||||||
const setForm = $("#setVaultForm");
|
|
||||||
const unlockForm = $("#unlockVaultForm");
|
|
||||||
const status = $("#vaultStatus");
|
|
||||||
$("#vaultLocked").classList.remove("hidden");
|
|
||||||
$("#vaultUnlocked").classList.add("hidden");
|
|
||||||
if (!ensureCrypto(status)) {
|
|
||||||
setForm.classList.add("hidden");
|
|
||||||
unlockForm.classList.add("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!meta) {
|
|
||||||
status.textContent = "Proteja suas senhas com uma senha mestra.";
|
|
||||||
setForm.classList.remove("hidden");
|
|
||||||
unlockForm.classList.add("hidden");
|
|
||||||
} else {
|
|
||||||
status.textContent = "Informe sua senha mestra para desbloquear.";
|
|
||||||
setForm.classList.add("hidden");
|
|
||||||
unlockForm.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unlockVault(password) {
|
|
||||||
if (!ensureCrypto($("#vaultStatus"))) return;
|
|
||||||
const meta = getVault();
|
|
||||||
if (!meta) throw new Error("Cofre não configurado");
|
|
||||||
const key = await deriveKey(password, meta.salt, meta.iterations || VAULT_ITERS);
|
|
||||||
try {
|
|
||||||
const check = await decryptString(key, meta.verification);
|
|
||||||
if (check !== "ok") throw new Error("Senha inválida");
|
|
||||||
vaultKey = key; // mantido em memória
|
|
||||||
$("#vaultLocked").classList.add("hidden");
|
|
||||||
$("#vaultUnlocked").classList.remove("hidden");
|
|
||||||
renderCreds($("#searchCreds").value);
|
|
||||||
} catch {
|
|
||||||
alert("Senha mestra incorreta.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function lockVault() {
|
|
||||||
vaultKey = null;
|
|
||||||
$("#vaultUnlockPass").value = "";
|
|
||||||
$("#credsList").innerHTML = "";
|
|
||||||
renderVaultGate();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderCreds(filter = "") {
|
async function renderCreds(filter = "") {
|
||||||
if (state.useApi) await refreshCreds();
|
if (state.useApi) await refreshCreds();
|
||||||
const listEl = $("#credsList");
|
const listEl = $("#credsList");
|
||||||
@@ -544,17 +430,19 @@ async function renderCreds(filter = "") {
|
|||||||
const c = items2[idx];
|
const c = items2[idx];
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
if (btn) {
|
if (btn) {
|
||||||
// Mantido por compatibilidade caso algum botão exista
|
|
||||||
if (!vaultKey) { alert("Cofre bloqueado."); return; }
|
|
||||||
const act = btn.dataset.act;
|
const act = btn.dataset.act;
|
||||||
if (act === "open" && c.url) {
|
if (act === "open" && c.url) {
|
||||||
window.open(c.url, "_blank", "noopener");
|
window.open(c.url, "_blank", "noopener");
|
||||||
} else if (act === "copyuser" && c.usuario) {
|
} else if (act === "copyuser" && c.usuario) {
|
||||||
copyText(c.usuario);
|
copyText(c.usuario);
|
||||||
} else if (act === "copypass") {
|
} else if (act === "copypass") {
|
||||||
try { const pass = await decryptString(vaultKey, c.passwordEnc); copyText(pass); } catch { alert("Falha ao descriptografar."); }
|
const pass = getCredPassword(c);
|
||||||
|
if (!pass) { alert("Senha indisponível."); return; }
|
||||||
|
copyText(pass);
|
||||||
} else if (act === "reveal") {
|
} else if (act === "reveal") {
|
||||||
try { const pass = await decryptString(vaultKey, c.passwordEnc); alert(`Senha: ${pass}`); } catch { alert("Falha ao descriptografar."); }
|
const pass = getCredPassword(c);
|
||||||
|
if (!pass) { alert("Senha indisponível."); return; }
|
||||||
|
alert(`Senha: ${pass}`);
|
||||||
} else if (act === "edit") {
|
} else if (act === "edit") {
|
||||||
openCredModal(c);
|
openCredModal(c);
|
||||||
} else if (act === "del") {
|
} else if (act === "del") {
|
||||||
@@ -569,8 +457,6 @@ async function renderCreds(filter = "") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clique no card abre visualização
|
|
||||||
if (!vaultKey) { alert("Cofre bloqueado."); return; }
|
|
||||||
openCredView(c, canWrite);
|
openCredView(c, canWrite);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -580,7 +466,7 @@ function openCredModal(c) {
|
|||||||
$("#credId").value = c?.id || "";
|
$("#credId").value = c?.id || "";
|
||||||
$("#credNome").value = c?.nome || "";
|
$("#credNome").value = c?.nome || "";
|
||||||
$("#credUsuario").value = c?.usuario || "";
|
$("#credUsuario").value = c?.usuario || "";
|
||||||
$("#credSenha").value = ""; // nunca preencher
|
$("#credSenha").value = getCredPassword(c);
|
||||||
$("#credUrl").value = c?.url || "";
|
$("#credUrl").value = c?.url || "";
|
||||||
$("#credNotas").value = c?.notas || "";
|
$("#credNotas").value = c?.notas || "";
|
||||||
showModal("credModal");
|
showModal("credModal");
|
||||||
@@ -616,11 +502,15 @@ function openCredView(c, canWrite) {
|
|||||||
|
|
||||||
btnOpen.onclick = () => { if (c.url) window.open(c.url, '_blank', 'noopener'); };
|
btnOpen.onclick = () => { if (c.url) window.open(c.url, '_blank', 'noopener'); };
|
||||||
btnCopyUser.onclick = () => { if (c.usuario) copyText(c.usuario); };
|
btnCopyUser.onclick = () => { if (c.usuario) copyText(c.usuario); };
|
||||||
btnCopyPass.onclick = async () => {
|
btnCopyPass.onclick = () => {
|
||||||
try { const pass = await decryptString(vaultKey, c.passwordEnc); copyText(pass); } catch { alert('Falha ao descriptografar.'); }
|
const pass = getCredPassword(c);
|
||||||
|
if (!pass) { alert('Senha indisponível.'); return; }
|
||||||
|
copyText(pass);
|
||||||
};
|
};
|
||||||
btnReveal.onclick = async () => {
|
btnReveal.onclick = () => {
|
||||||
try { const pass = await decryptString(vaultKey, c.passwordEnc); alert(`Senha: ${pass}`); } catch { alert('Falha ao descriptografar.'); }
|
const pass = getCredPassword(c);
|
||||||
|
if (!pass) { alert('Senha indisponível.'); return; }
|
||||||
|
alert(`Senha: ${pass}`);
|
||||||
};
|
};
|
||||||
btnEdit.onclick = () => { hideModal('credViewModal'); openCredModal(c); };
|
btnEdit.onclick = () => { hideModal('credViewModal'); openCredModal(c); };
|
||||||
btnDelete.onclick = async () => {
|
btnDelete.onclick = async () => {
|
||||||
@@ -647,7 +537,6 @@ function exportAll() {
|
|||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
theme: document.documentElement.getAttribute("data-theme"),
|
theme: document.documentElement.getAttribute("data-theme"),
|
||||||
units: loadUnits(),
|
units: loadUnits(),
|
||||||
vault: getVault(),
|
|
||||||
creds: loadCreds(),
|
creds: loadCreds(),
|
||||||
notes: loadNotes(),
|
notes: loadNotes(),
|
||||||
};
|
};
|
||||||
@@ -672,12 +561,10 @@ function importAll(file) {
|
|||||||
if (data.theme) { localStorage.setItem(STORAGE.THEME, data.theme); document.documentElement.setAttribute("data-theme", data.theme); }
|
if (data.theme) { localStorage.setItem(STORAGE.THEME, data.theme); document.documentElement.setAttribute("data-theme", data.theme); }
|
||||||
if (Array.isArray(data.units)) saveUnits(data.units);
|
if (Array.isArray(data.units)) saveUnits(data.units);
|
||||||
if (Array.isArray(data.creds)) saveCreds(data.creds);
|
if (Array.isArray(data.creds)) saveCreds(data.creds);
|
||||||
if (data.vault && data.vault.salt && data.vault.verification) setVault(data.vault);
|
|
||||||
if (Array.isArray(data.notes)) saveNotes(data.notes);
|
if (Array.isArray(data.notes)) saveNotes(data.notes);
|
||||||
alert("Importação concluída.");
|
alert("Importação concluída.");
|
||||||
renderUnits($("#searchUnidades").value);
|
renderUnits($("#searchUnidades").value);
|
||||||
renderNotes($("#searchNotes").value);
|
renderNotes($("#searchNotes").value);
|
||||||
lockVault(); // força rechecagem do cofre
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Arquivo inválido.");
|
alert("Arquivo inválido.");
|
||||||
}
|
}
|
||||||
@@ -706,7 +593,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!state.useApi || state.auth?.user) {
|
if (!state.useApi || state.auth?.user) {
|
||||||
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
|
await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]);
|
||||||
}
|
}
|
||||||
updateStorageInfo();
|
updateStorageInfo();
|
||||||
applyPermissionsUI();
|
applyPermissionsUI();
|
||||||
@@ -757,14 +644,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
if (btn) hideModal(btn.getAttribute("data-close-modal"));
|
if (btn) hideModal(btn.getAttribute("data-close-modal"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Senhas/Cofre
|
// Senhas
|
||||||
renderVaultGate();
|
|
||||||
$("#lockVaultBtn").addEventListener("click", lockVault);
|
|
||||||
$("#searchCreds").addEventListener("input", ev => renderCreds(ev.target.value));
|
$("#searchCreds").addEventListener("input", ev => renderCreds(ev.target.value));
|
||||||
$("#addCredBtn").addEventListener("click", () => {
|
$("#addCredBtn").addEventListener("click", () => openCredModal());
|
||||||
if (!vaultKey) { alert("Desbloqueie o cofre primeiro."); return; }
|
|
||||||
openCredModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notas
|
// Notas
|
||||||
$("#addNoteBtn").addEventListener("click", () => openNoteModal());
|
$("#addNoteBtn").addEventListener("click", () => openNoteModal());
|
||||||
@@ -821,50 +703,19 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
renderNotes($("#searchNotes").value);
|
renderNotes($("#searchNotes").value);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#setVaultForm").addEventListener("submit", async (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
if (!ensureCrypto($("#vaultStatus"))) return;
|
|
||||||
const p1 = $("#vaultPass1").value;
|
|
||||||
const p2 = $("#vaultPass2").value;
|
|
||||||
if (!p1 || p1 !== p2) { alert("Senhas não conferem."); return; }
|
|
||||||
const salt = ab2b64(crypto.getRandomValues(new Uint8Array(16)).buffer);
|
|
||||||
const key = await deriveKey(p1, salt, VAULT_ITERS);
|
|
||||||
const verification = await encryptString(key, "ok");
|
|
||||||
const meta = { salt, verification, iterations: VAULT_ITERS, v: 1 };
|
|
||||||
if (state.useApi) {
|
|
||||||
await apiFetch('/api/vault', { method: 'PUT', body: JSON.stringify(meta) });
|
|
||||||
await refreshVault();
|
|
||||||
} else {
|
|
||||||
setVault(meta);
|
|
||||||
}
|
|
||||||
alert("Cofre configurado. Guarde sua senha mestra!");
|
|
||||||
$("#vaultPass1").value = ""; $("#vaultPass2").value = "";
|
|
||||||
unlockVault(p1);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#unlockVaultForm").addEventListener("submit", async (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
const p = $("#vaultUnlockPass").value;
|
|
||||||
await unlockVault(p);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#credForm").addEventListener("submit", async (ev) => {
|
$("#credForm").addEventListener("submit", async (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!vaultKey) { alert("Cofre bloqueado."); return; }
|
|
||||||
const id = $("#credId").value || uid();
|
const id = $("#credId").value || uid();
|
||||||
const list = loadCreds();
|
const list = loadCreds();
|
||||||
const idx = list.findIndex(x => x.id === id);
|
const idx = list.findIndex(x => x.id === id);
|
||||||
const existing = idx >= 0 ? list[idx] : null;
|
const passwordPlain = $("#credSenha").value.trim();
|
||||||
const passwordPlain = $("#credSenha").value; // pode estar vazio ao editar
|
|
||||||
let passwordEnc = existing?.passwordEnc;
|
|
||||||
if (passwordPlain) passwordEnc = await encryptString(vaultKey, passwordPlain);
|
|
||||||
const item = {
|
const item = {
|
||||||
id,
|
id,
|
||||||
nome: $("#credNome").value.trim(),
|
nome: $("#credNome").value.trim(),
|
||||||
usuario: $("#credUsuario").value.trim(),
|
usuario: $("#credUsuario").value.trim(),
|
||||||
url: $("#credUrl").value.trim(),
|
url: $("#credUrl").value.trim(),
|
||||||
notas: $("#credNotas").value.trim(),
|
notas: $("#credNotas").value.trim(),
|
||||||
passwordEnc,
|
passwordEnc: passwordPlain,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
};
|
};
|
||||||
if (!item.nome) { alert("Informe o nome."); return; }
|
if (!item.nome) { alert("Informe o nome."); return; }
|
||||||
@@ -901,8 +752,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
if (!confirm("Importar backup e substituir dados atuais no servidor?")) return;
|
if (!confirm("Importar backup e substituir dados atuais no servidor?")) return;
|
||||||
await apiFetch('/api/import', { method: 'POST', body: JSON.stringify(data) });
|
await apiFetch('/api/import', { method: 'POST', body: JSON.stringify(data) });
|
||||||
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
|
await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]);
|
||||||
lockVault();
|
|
||||||
renderUnits($("#searchUnidades").value); // re-render
|
renderUnits($("#searchUnidades").value); // re-render
|
||||||
renderNotes($("#searchNotes").value);
|
renderNotes($("#searchNotes").value);
|
||||||
} else {
|
} else {
|
||||||
@@ -926,7 +776,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
state.auth = { user: data.user };
|
state.auth = { user: data.user };
|
||||||
hideLogin();
|
hideLogin();
|
||||||
applyPermissionsUI();
|
applyPermissionsUI();
|
||||||
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
|
await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]);
|
||||||
if (canMaster()) await renderUsers();
|
if (canMaster()) await renderUsers();
|
||||||
renderUnits(document.getElementById('searchUnidades').value);
|
renderUnits(document.getElementById('searchUnidades').value);
|
||||||
renderNotes(document.getElementById('searchNotes').value);
|
renderNotes(document.getElementById('searchNotes').value);
|
||||||
@@ -940,6 +790,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
if (!state.useApi || state.auth?.user) {
|
if (!state.useApi || state.auth?.user) {
|
||||||
renderUnits("");
|
renderUnits("");
|
||||||
renderNotes("");
|
renderNotes("");
|
||||||
|
renderCreds("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -135,8 +135,6 @@ label span { display: block; font-size: 13px; color: var(--muted); margin-bottom
|
|||||||
.modal-body { padding: 14px; display: grid; gap: 12px; }
|
.modal-body { padding: 14px; display: grid; gap: 12px; }
|
||||||
.modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
|
.modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
|
||||||
|
|
||||||
.vault-lock { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; display: grid; gap: 10px; max-width: 720px; margin: 18px auto; }
|
|
||||||
.vault-form { display: grid; gap: 12px; }
|
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
.app-footer { padding: 18px; text-align: center; color: var(--muted); }
|
.app-footer { padding: 18px; text-align: center; color: var(--muted); }
|
||||||
|
|||||||
29
index.html
29
index.html
@@ -46,44 +46,15 @@
|
|||||||
|
|
||||||
<!-- Senhas -->
|
<!-- Senhas -->
|
||||||
<section id="view-senhas" class="view" aria-labelledby="tab-senhas">
|
<section id="view-senhas" class="view" aria-labelledby="tab-senhas">
|
||||||
<div id="vaultLocked" class="vault-lock">
|
|
||||||
<h2>Cofre de Senhas</h2>
|
|
||||||
<p id="vaultStatus">Proteja suas senhas com uma senha mestra.</p>
|
|
||||||
<form id="setVaultForm" class="vault-form hidden">
|
|
||||||
<div class="grid">
|
|
||||||
<label>
|
|
||||||
<span>Senha Mestra</span>
|
|
||||||
<input type="password" id="vaultPass1" class="input" autocomplete="new-password" required />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Confirmar Senha</span>
|
|
||||||
<input type="password" id="vaultPass2" class="input" autocomplete="new-password" required />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button class="btn primary" type="submit">Definir senha mestra</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form id="unlockVaultForm" class="vault-form hidden">
|
|
||||||
<label>
|
|
||||||
<span>Senha Mestra</span>
|
|
||||||
<input type="password" id="vaultUnlockPass" class="input" autocomplete="current-password" required />
|
|
||||||
</label>
|
|
||||||
<button class="btn primary" type="submit">Desbloquear</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="vaultUnlocked" class="hidden">
|
|
||||||
<div class="view-bar">
|
<div class="view-bar">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button id="addCredBtn" class="btn primary">+ Nova Credencial</button>
|
<button id="addCredBtn" class="btn primary">+ Nova Credencial</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<input id="searchCreds" class="input" placeholder="Buscar por nome, usuário ou URL" />
|
<input id="searchCreds" class="input" placeholder="Buscar por nome, usuário ou URL" />
|
||||||
<button id="lockVaultBtn" class="btn" title="Bloquear cofre">Bloquear</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="credsList" class="cards"></div>
|
<div id="credsList" class="cards"></div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Notas -->
|
<!-- Notas -->
|
||||||
|
|||||||
Reference in New Issue
Block a user