300 lines
12 KiB
JavaScript
300 lines
12 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
|
||
const PasswordManager = () => {
|
||
const [passwords, setPasswords] = useState([]);
|
||
const [showAddForm, setShowAddForm] = useState(false);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [newPassword, setNewPassword] = useState({
|
||
title: '',
|
||
server: '',
|
||
username: '',
|
||
password: '',
|
||
category: 'server',
|
||
notes: ''
|
||
});
|
||
|
||
useEffect(() => {
|
||
loadPasswords();
|
||
}, []);
|
||
|
||
const loadPasswords = async () => {
|
||
if (window.electronAPI) {
|
||
const result = await window.electronAPI.loadPasswords();
|
||
if (result.success) {
|
||
setPasswords(result.passwords || []);
|
||
}
|
||
}
|
||
};
|
||
|
||
const savePasswords = async (passwordsList) => {
|
||
if (window.electronAPI) {
|
||
await window.electronAPI.savePasswords(passwordsList);
|
||
}
|
||
};
|
||
|
||
const addPassword = async () => {
|
||
if (!newPassword.title) {
|
||
alert('Заполните название');
|
||
return;
|
||
}
|
||
|
||
const password = {
|
||
...newPassword,
|
||
id: Date.now().toString(),
|
||
createdAt: new Date().toISOString()
|
||
};
|
||
|
||
const updatedPasswords = [...passwords, password];
|
||
setPasswords(updatedPasswords);
|
||
await savePasswords(updatedPasswords);
|
||
setShowAddForm(false);
|
||
setNewPassword({
|
||
title: '',
|
||
server: '',
|
||
username: '',
|
||
password: '',
|
||
category: 'server',
|
||
notes: ''
|
||
});
|
||
};
|
||
|
||
const deletePassword = async (passwordId) => {
|
||
if (confirm('Удалить эту запись?')) {
|
||
const updatedPasswords = passwords.filter(p => p.id !== passwordId);
|
||
setPasswords(updatedPasswords);
|
||
await savePasswords(updatedPasswords);
|
||
}
|
||
};
|
||
|
||
const copyToClipboard = async (text) => {
|
||
if (window.electronAPI) {
|
||
await window.electronAPI.copyToClipboard(text);
|
||
alert('Скопировано в буфер обмена');
|
||
}
|
||
};
|
||
|
||
const generatePassword = () => {
|
||
const length = 16;
|
||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
||
let password = "";
|
||
for (let i = 0; i < length; i++) {
|
||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||
}
|
||
setNewPassword({...newPassword, password});
|
||
};
|
||
|
||
const filteredPasswords = passwords.filter(password =>
|
||
password.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
password.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
password.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||
);
|
||
|
||
return (
|
||
<div className="p-4 md:p-6">
|
||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
|
||
<div>
|
||
<h1 className="text-xl md:text-2xl font-bold text-white">Менеджер паролей</h1>
|
||
<p className="text-gray-400 text-sm md:text-base">Безопасное хранение учетных данных</p>
|
||
</div>
|
||
<div className="flex flex-col md:flex-row gap-3 w-full md:w-auto">
|
||
<div className="relative w-full md:w-64">
|
||
<input
|
||
type="text"
|
||
placeholder="Поиск паролей..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full pl-3 pr-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowAddForm(true)}
|
||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto text-sm"
|
||
>
|
||
+ Добавить запись
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{showAddForm && (
|
||
<div className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg mb-4 md:mb-6 border border-gray-700">
|
||
<h3 className="text-lg font-semibold mb-4 text-white">Добавить новую запись</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||
Название *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={newPassword.title}
|
||
onChange={(e) => setNewPassword({...newPassword, title: e.target.value})}
|
||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
placeholder="Root доступ к серверу"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||
Сервер
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={newPassword.server}
|
||
onChange={(e) => setNewPassword({...newPassword, server: e.target.value})}
|
||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
placeholder="server.example.com"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||
Категория
|
||
</label>
|
||
<select
|
||
value={newPassword.category}
|
||
onChange={(e) => setNewPassword({...newPassword, category: e.target.value})}
|
||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
>
|
||
<option value="server">Сервер</option>
|
||
<option value="database">База данных</option>
|
||
<option value="web">Веб-сайт</option>
|
||
<option value="other">Другое</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||
Имя пользователя
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={newPassword.username}
|
||
onChange={(e) => setNewPassword({...newPassword, username: e.target.value})}
|
||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
placeholder="admin"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||
Пароль
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={newPassword.password}
|
||
onChange={(e) => setNewPassword({...newPassword, password: e.target.value})}
|
||
className="flex-1 bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
placeholder="Пароль"
|
||
/>
|
||
<button
|
||
onClick={generatePassword}
|
||
className="px-3 bg-primary-600 text-white rounded-lg hover:bg-primary-500 text-sm"
|
||
>
|
||
🎲
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="md:col-span-2">
|
||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||
Заметки
|
||
</label>
|
||
<textarea
|
||
value={newPassword.notes}
|
||
onChange={(e) => setNewPassword({...newPassword, notes: e.target.value})}
|
||
rows="3"
|
||
className="w-full bg-gray-700 border border-gray-600 rounded-lg p-3 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||
placeholder="Дополнительная информация..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-3 mt-4">
|
||
<button
|
||
onClick={() => setShowAddForm(false)}
|
||
className="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors w-full md:w-auto text-sm"
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
onClick={addPassword}
|
||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto text-sm"
|
||
>
|
||
Сохранить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{filteredPasswords.length === 0 ? (
|
||
<div className="text-center py-8 md:py-12">
|
||
<div className="text-gray-400 text-4xl md:text-6xl mb-4">🔐</div>
|
||
<h3 className="text-lg font-medium text-white">Нет сохраненных паролей</h3>
|
||
<p className="text-gray-400 mt-1">
|
||
{searchTerm ? 'Ничего не найдено' : 'Добавьте вашу первую запись'}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||
{filteredPasswords.map(password => (
|
||
<div key={password.id} className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg border border-gray-700">
|
||
<div className="flex justify-between items-start mb-3 md:mb-4">
|
||
<div>
|
||
<h3 className="font-semibold text-white text-base md:text-lg">{password.title}</h3>
|
||
{password.server && (
|
||
<p className="text-gray-400 text-sm mt-1">{password.server}</p>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => deletePassword(password.id)}
|
||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-3 md:space-y-4">
|
||
<div className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-400">Пользователь:</span>
|
||
<div className="flex items-center space-x-2">
|
||
<span className="text-sm text-white font-mono">{password.username}</span>
|
||
<button
|
||
onClick={() => copyToClipboard(password.username)}
|
||
className="text-gray-400 hover:text-primary-500 transition-colors"
|
||
>
|
||
📋
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-400">Пароль:</span>
|
||
<div className="flex items-center space-x-2">
|
||
<span className="text-sm text-white font-mono">••••••••</span>
|
||
<button
|
||
onClick={() => copyToClipboard(password.password)}
|
||
className="text-gray-400 hover:text-primary-500 transition-colors"
|
||
>
|
||
📋
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{password.notes && (
|
||
<div className="pt-3 border-t border-gray-700">
|
||
<p className="text-sm text-gray-400 italic">{password.notes}</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex justify-between items-center pt-3 border-t border-gray-700">
|
||
<span className="text-xs text-gray-500">
|
||
{new Date(password.createdAt).toLocaleDateString()}
|
||
</span>
|
||
<span className="text-xs px-2 py-1 bg-primary-500 text-white rounded">
|
||
{password.category}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PasswordManager; |