feat: initial commit - ServerManager Pro v2.0.0
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user