feat: Initialize License Tracker Pro v1.2 project
Adds initial project structure, dependencies, and basic configuration for the License Tracker Pro v1.2 application. This includes setting up Vite for the build process, defining core types, and incorporating essential UI components like icons and a timeline. The project is now ready for development.
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { User } from '../types';
|
||||
import { fetchUsers, createUser, deleteUser, updateUser } from '../graphService';
|
||||
import { TrashIcon, PlusIcon, UserIcon, EditIcon, SettingsIcon } from './Icons';
|
||||
|
||||
const UserManagement: React.FC = () => {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [showDBHelper, setShowDBHelper] = useState(false);
|
||||
|
||||
// Form State
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
role: 'editor' as 'admin'|'editor',
|
||||
auth_source: 'local' as 'local'|'azure'|'ldap',
|
||||
sendEmail: true
|
||||
});
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [editingUserId, setEditingUserId] = useState<number | null>(null);
|
||||
|
||||
const loadUsers = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchUsers();
|
||||
setUsers(data);
|
||||
setError('');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
const handleCreateOrUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const payload = { ...formData };
|
||||
// If editing, only send password if it has been typed
|
||||
if (editingUserId) {
|
||||
if (!payload.password) {
|
||||
delete (payload as any).password;
|
||||
}
|
||||
await updateUser(editingUserId, payload);
|
||||
} else {
|
||||
await createUser(payload);
|
||||
}
|
||||
resetForm();
|
||||
loadUsers();
|
||||
} catch (err: any) {
|
||||
alert('Operation Failed: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({ username: '', password: '', email: '', role: 'editor', auth_source: 'local', sendEmail: true });
|
||||
setIsFormVisible(false);
|
||||
setEditingUserId(null);
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUserId(user.id);
|
||||
setFormData({
|
||||
username: user.username,
|
||||
password: '',
|
||||
email: user.email || '',
|
||||
role: user.role,
|
||||
auth_source: user.auth_source || 'local',
|
||||
sendEmail: false
|
||||
});
|
||||
setIsFormVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (id === 1) return;
|
||||
if (!window.confirm('Are you sure you want to delete this user?')) return;
|
||||
try {
|
||||
await deleteUser(id);
|
||||
loadUsers();
|
||||
} catch (err: any) {
|
||||
alert('Error deleting user: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && users.length === 0) return <div className="text-center py-8 text-cyan-400 font-mono text-sm animate-pulse">Establishing Secure DB Connection...</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 bg-slate-800 rounded-lg shadow-lg overflow-hidden h-fit border border-slate-700">
|
||||
<div className="p-6 border-b border-slate-700 flex justify-between items-center bg-slate-900/50">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-200">System Accounts</h2>
|
||||
<p className="text-sm text-gray-500 font-mono text-[10px] uppercase tracking-wider">MariaDB Production Instance</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowDBHelper(!showDBHelper)}
|
||||
className={`px-3 py-2 rounded flex items-center text-sm font-bold transition-all border ${showDBHelper ? 'bg-red-600 text-white border-red-500' : 'bg-slate-700 hover:bg-slate-600 text-slate-300 border-slate-600'}`}
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4 mr-2"/> {showDBHelper ? 'Close Fix' : 'Fix Login Error'}
|
||||
</button>
|
||||
{!isFormVisible && (
|
||||
<button
|
||||
onClick={() => setIsFormVisible(true)}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white px-3 py-2 rounded flex items-center text-sm font-bold shadow-lg transition-all active:scale-95"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 mr-2"/> Add User
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="p-4 bg-red-900/20 text-red-400 border-b border-red-900/50 text-xs font-mono">Error: {error}</div>}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-gray-400">
|
||||
<thead className="bg-slate-900/80 uppercase text-[10px] font-black tracking-widest">
|
||||
<tr>
|
||||
<th className="px-6 py-3">Username</th>
|
||||
<th className="px-6 py-3">Privilege</th>
|
||||
<th className="px-6 py-3">Source</th>
|
||||
<th className="px-6 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700">
|
||||
{users.map(user => (
|
||||
<tr key={user.id} className="hover:bg-slate-700/50 transition-colors">
|
||||
<td className="px-6 py-4 font-medium text-white flex items-center">
|
||||
<UserIcon className="w-4 h-4 mr-2 text-gray-500"/>
|
||||
{user.username}
|
||||
{user.id === 1 && <span className="ml-2 text-[8px] bg-cyan-900/40 text-cyan-400 border border-cyan-800/50 px-1.5 py-0.5 rounded font-black tracking-tighter">PRIMARY ADMIN</span>}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${user.role === 'admin' ? 'bg-purple-900/40 text-purple-400' : 'bg-slate-900/60 text-slate-400'}`}>
|
||||
{user.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono text-xs uppercase tracking-tighter text-slate-500">{user.auth_source}</td>
|
||||
<td className="px-6 py-4 text-right flex justify-end gap-2">
|
||||
<button onClick={() => handleEdit(user)} className="text-cyan-400 hover:text-cyan-300 p-2 hover:bg-cyan-900/20 rounded-full transition-all" title="Modify Credentials">
|
||||
<EditIcon className="w-4 h-4"/>
|
||||
</button>
|
||||
{user.id !== 1 && (
|
||||
<button onClick={() => handleDelete(user.id)} className="text-red-400 hover:text-red-300 p-2 hover:bg-red-900/20 rounded-full transition-all" title="Remove Account">
|
||||
<TrashIcon className="w-4 h-4"/>
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFormVisible && (
|
||||
<div className="lg:col-span-1 bg-slate-800 rounded-lg shadow-lg p-6 h-fit border border-slate-700 animate-in slide-in-from-right duration-300">
|
||||
<div className="flex justify-between items-center mb-6 border-b border-slate-700 pb-2">
|
||||
<h3 className="text-lg font-bold text-cyan-400">
|
||||
{editingUserId ? 'Edit Account' : 'New Account'}
|
||||
</h3>
|
||||
<button onClick={resetForm} className="text-gray-500 hover:text-gray-300 text-xl">×</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateOrUpdate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-widest mb-1 ml-1">Username</label>
|
||||
<input
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-gray-200 focus:outline-none focus:border-cyan-500 transition-all"
|
||||
value={formData.username}
|
||||
onChange={e => setFormData({...formData, username: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-widest mb-1 ml-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-gray-200 focus:outline-none focus:border-cyan-500 transition-all"
|
||||
value={formData.email}
|
||||
onChange={e => setFormData({...formData, email: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-widest mb-1 ml-1">
|
||||
{editingUserId ? 'New Password (Blank to Keep)' : 'Password'}
|
||||
</label>
|
||||
<input
|
||||
required={!editingUserId}
|
||||
type="password"
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-gray-200 focus:outline-none focus:border-cyan-500 transition-all"
|
||||
value={formData.password}
|
||||
onChange={e => setFormData({...formData, password: e.target.value})}
|
||||
placeholder={editingUserId ? "••••••••" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-widest mb-1 ml-1">Role</label>
|
||||
<select
|
||||
disabled={editingUserId === 1}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-gray-200 focus:outline-none focus:border-cyan-500 transition-all disabled:opacity-50"
|
||||
value={formData.role}
|
||||
onChange={e => setFormData({...formData, role: e.target.value as any})}
|
||||
>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-gray-500 uppercase tracking-widest mb-1 ml-1">Auth Source</label>
|
||||
<select
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-gray-200 focus:outline-none focus:border-cyan-500 transition-all"
|
||||
value={formData.auth_source}
|
||||
onChange={e => setFormData({...formData, auth_source: e.target.value as any})}
|
||||
>
|
||||
<option value="local">Local</option>
|
||||
<option value="ldap">LDAP</option>
|
||||
<option value="azure">Azure</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex gap-3">
|
||||
<button type="button" onClick={resetForm} className="flex-1 py-3 bg-slate-700/50 hover:bg-slate-700 text-gray-400 font-bold rounded-xl transition-all border border-slate-600">Cancel</button>
|
||||
<button type="submit" className="flex-1 py-3 bg-cyan-600 hover:bg-cyan-500 text-white rounded-xl font-bold transition-all shadow-lg active:scale-95">
|
||||
{editingUserId ? 'Apply Changes' : 'Create User'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDBHelper && (
|
||||
<div className="bg-slate-900 border border-red-900/50 p-6 rounded-lg animate-in fade-in slide-in-from-bottom duration-300">
|
||||
<h3 className="text-red-400 font-bold mb-2 flex items-center">
|
||||
<SettingsIcon className="w-5 h-5 mr-2"/> Fix: Apply Verified Working Hash
|
||||
</h3>
|
||||
<p className="text-xs text-gray-400 mb-4 leading-relaxed">
|
||||
Use the exact Blowfish hash you verified for the 'admin' user recovery.
|
||||
<br/><br/>
|
||||
<b>Confirmed Hash for 'admin123':</b>
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase mb-1">Step 1: Open Terminal</p>
|
||||
<code className="block bg-black p-3 rounded border border-slate-700 text-green-400 text-xs">
|
||||
docker compose exec db mariadb -u root -p
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase mb-1">Step 2: Update Password Field</p>
|
||||
<code className="block bg-black p-3 rounded border border-slate-700 text-cyan-400 text-xs whitespace-pre-wrap break-all">
|
||||
{`USE license_tracker;
|
||||
|
||||
-- Apply the verified Blowfish hash ($2a$ format) for password 'admin123':
|
||||
UPDATE users SET password = '$2a$12$.UrnKUhUx9ypgARYW7VF2.okk.PyKqzSlCflAi5aDKhSrbKU.ciii' WHERE username = 'admin';
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;`}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagement;
|
||||
Reference in New Issue
Block a user