import { useEffect, useState } from 'react'; import Layout from '../../components/Layout'; import { supabase } from '../../lib/supabase'; import { useAuth } from '../../context/AuthContext'; function emptyForm() { return { service_name: '', service_url: '', username: '', password: '', notes: '', }; } function sortEntries(entries) { return [...entries].sort((a, b) => a.service_name.localeCompare(b.service_name, undefined, { sensitivity: 'base' })); } function formatDate(value) { if (!value) return 'Unknown'; return new Date(value).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); } function normalizeUrl(value) { const raw = String(value || '').trim(); if (!raw) return ''; if (/^https?:\/\//i.test(raw)) return raw; return `https://${raw}`; } async function invokeCrypto(action, payload) { const { data: sessionData } = await supabase.auth.getSession(); const accessToken = sessionData.session?.access_token; const response = await fetch('/api/fourge-passwords-crypto', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify({ action, ...payload }), }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || `Vault operation failed (${response.status})`); if (data?.error) throw new Error(data.error); return data; } export default function FourgePasswords() { const { currentUser } = useAuth(); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [status, setStatus] = useState(''); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyForm()); const [revealed, setRevealed] = useState({}); async function loadEntries() { setLoading(true); const { data, error } = await supabase .from('fourge_passwords') .select('id, service_name, service_url, username, notes, created_at, updated_at, created_by') .order('service_name', { ascending: true }); if (error) { setStatus(`Failed to load passwords: ${error.message}`); setEntries([]); setLoading(false); return; } setEntries(sortEntries(data || [])); setLoading(false); setStatus(''); } useEffect(() => { loadEntries(); }, []); function beginCreate() { setEditingId('new'); setForm(emptyForm()); setStatus(''); } function beginEdit(entry) { setEditingId(entry.id); setForm({ service_name: entry.service_name || '', service_url: entry.service_url || '', username: entry.username || '', password: '', notes: entry.notes || '', }); setStatus(''); } function cancelEdit() { setEditingId(null); setForm(emptyForm()); } async function handleSave(event) { event.preventDefault(); if (!form.service_name.trim() || !form.username.trim()) { setStatus('Service and username are required.'); return; } if (editingId === 'new' && !form.password) { setStatus('Password is required for new entries.'); return; } setSaving(true); setStatus(''); try { let encryptedPayload = null; if (form.password) { encryptedPayload = await invokeCrypto('encrypt', { plaintext: form.password }); } const payload = { service_name: form.service_name.trim(), service_url: form.service_url.trim(), username: form.username.trim(), notes: form.notes.trim(), updated_at: new Date().toISOString(), }; if (encryptedPayload) { payload.encrypted_password = encryptedPayload.ciphertext; payload.password_iv = encryptedPayload.iv; } if (editingId === 'new') { payload.created_by = currentUser?.id || null; const { data, error } = await supabase .from('fourge_passwords') .insert(payload) .select('id, service_name, service_url, username, notes, created_at, updated_at, created_by') .single(); if (error) throw error; setEntries(prev => sortEntries([...prev, data])); setStatus('Password saved.'); } else { const { data, error } = await supabase .from('fourge_passwords') .update(payload) .eq('id', editingId) .select('id, service_name, service_url, username, notes, created_at, updated_at, created_by') .single(); if (error) throw error; setEntries(prev => sortEntries(prev.map(entry => entry.id === editingId ? data : entry))); setStatus('Password updated.'); } cancelEdit(); } catch (error) { setStatus(`Save failed: ${error.message}`); } finally { setSaving(false); } } async function handleDelete(entry) { if (!window.confirm(`Delete the password entry for "${entry.service_name}"?`)) return; setStatus(''); const { error } = await supabase.from('fourge_passwords').delete().eq('id', entry.id); if (error) { setStatus(`Delete failed: ${error.message}`); return; } setEntries(prev => prev.filter(item => item.id !== entry.id)); setRevealed(prev => { const next = { ...prev }; delete next[entry.id]; return next; }); setStatus('Password deleted.'); } async function handleReveal(entry) { if (revealed[entry.id]) { setRevealed(prev => { const next = { ...prev }; delete next[entry.id]; return next; }); return; } setStatus(''); try { const { data, error } = await supabase .from('fourge_passwords') .select('encrypted_password, password_iv') .eq('id', entry.id) .single(); if (error) throw error; const decrypted = await invokeCrypto('decrypt', { ciphertext: data.encrypted_password, iv: data.password_iv, }); setRevealed(prev => ({ ...prev, [entry.id]: decrypted.plaintext })); } catch (error) { setStatus(`Reveal failed: ${error.message}`); } } async function handleCopy(entry) { try { let password = revealed[entry.id]; if (!password) { const { data, error } = await supabase .from('fourge_passwords') .select('encrypted_password, password_iv') .eq('id', entry.id) .single(); if (error) throw error; const decrypted = await invokeCrypto('decrypt', { ciphertext: data.encrypted_password, iv: data.password_iv, }); password = decrypted.plaintext; setRevealed(prev => ({ ...prev, [entry.id]: password })); } await navigator.clipboard.writeText(password); setStatus(`Copied password for ${entry.service_name}.`); } catch (error) { setStatus(`Copy failed: ${error.message}`); } } return (
Fourge Passwords
Encrypted team-only vault entries for services, logins, and internal access.
{(editingId || status) && (
{editingId && (
{editingId === 'new' ? 'Add Password' : 'Edit Password'}
setForm(prev => ({ ...prev, service_name: e.target.value }))} /> setForm(prev => ({ ...prev, service_url: e.target.value }))} /> setForm(prev => ({ ...prev, username: e.target.value }))} /> setForm(prev => ({ ...prev, password: e.target.value }))} />