This commit is contained in:
2026-03-19 14:36:35 +07:00
parent 6d7d86befd
commit 96635dbcf2
28 changed files with 4332 additions and 1683 deletions

145
public/js/api.js Normal file
View File

@@ -0,0 +1,145 @@
// API Helper - Centralized API calls
class API {
constructor() {
this.baseURL = '/api';
this.timeout = 10000;
}
async request(endpoint, options = {}) {
const {
method = 'GET',
headers = {},
body = null,
timeout = this.timeout
} = options;
const url = `${this.baseURL}${endpoint}`;
const config = {
method,
headers: {
'Content-Type': 'application/json',
...headers
}
};
if (body) {
config.body = JSON.stringify(body);
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...config,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
if (response.status === 401) {
// Redirect to login if unauthorized
window.location.hash = '#login';
throw new Error('Unauthorized. Please login again.');
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
// Auth endpoints
async login(email, password) {
return this.request('/auth/login', {
method: 'POST',
body: { email, password }
});
}
async register(name, email, password) {
return this.request('/auth/register', {
method: 'POST',
body: { name, email, password }
});
}
async logout() {
return this.request('/auth/logout', { method: 'POST' });
}
async getCurrentUser() {
return this.request('/auth/me');
}
// Users endpoints
async getUsers() {
return this.request('/users');
}
async createUser(userData) {
return this.request('/users', {
method: 'POST',
body: userData
});
}
async updateUser(userId, userData) {
return this.request(`/users/${userId}`, {
method: 'PATCH',
body: userData
});
}
async deleteUser(userId) {
return this.request(`/users/${userId}`, {
method: 'DELETE'
});
}
// Database endpoints
async getTables() {
return this.request('/db/tables');
}
async getTableData(tableName, limit = 100, offset = 0) {
return this.request(`/db/tables/${tableName}/data?limit=${limit}&offset=${offset}`);
}
async executeQuery(sql) {
return this.request('/db/query', {
method: 'POST',
body: { sql }
});
}
async getDatabaseStats() {
return this.request('/db/stats');
}
// Admin endpoints
async getSystemStats() {
return this.request('/admin/stats');
}
async getLogs(limit = 100) {
return this.request(`/admin/logs?limit=${limit}`);
}
async getBackups() {
return this.request('/admin/backups');
}
async createBackup() {
return this.request('/admin/backups', {
method: 'POST'
});
}
}
// Global API instance
const api = new API();

228
public/js/app.js Normal file
View File

@@ -0,0 +1,228 @@
// Main Application Handler
class Application {
constructor() {
this.init();
}
async init() {
// Wait for auth check
const isAuth = await auth.checkAuth();
if (isAuth) {
// Redirect to dashboard
if (window.location.hash === '' || window.location.hash === '#login') {
window.location.hash = '#dashboard';
}
} else {
// Redirect to login
window.location.hash = '#login';
}
}
loadDashboardData() {
this.loadStats();
this.loadTablesList();
}
async loadStats() {
try {
const stats = await api.getDatabaseStats();
document.getElementById('statsTableCount').textContent = stats.tableCount || 0;
document.getElementById('statsRecordCount').textContent = stats.recordCount || 0;
document.getElementById('statsUserCount').textContent = stats.userCount || 0;
document.getElementById('statsDbSize').textContent = stats.dbSize || '-';
} catch (error) {
console.error('Failed to load stats:', error);
}
}
async loadTablesList() {
try {
const tables = await api.getTables();
const tablesList = document.getElementById('tablesList');
if (tables.length === 0) {
tablesList.innerHTML = '<p class="text-slate-500 dark:text-slate-400 text-sm">Таблицы не найдены</p>';
return;
}
tablesList.innerHTML = tables.map(table => `
<div class="flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-800 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors cursor-pointer" onclick="window.location.hash='#tables'">
<div class="flex items-center gap-2">
<i data-lucide="table" class="w-4 h-4 text-blue-600 dark:text-blue-400"></i>
<span class="font-medium text-slate-900 dark:text-white">${table.name}</span>
</div>
<span class="text-xs text-slate-500 dark:text-slate-400">${table.recordCount || 0} записей</span>
</div>
`).join('');
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
} catch (error) {
console.error('Failed to load tables:', error);
}
}
loadAdminPanel() {
this.loadUsers();
}
async loadUsers() {
try {
const users = await api.getUsers();
const usersList = document.getElementById('usersList');
if (!users || users.length === 0) {
usersList.innerHTML = '<tr><td colspan="5" class="py-8 px-4 text-center text-slate-500 dark:text-slate-400">Пользователи не найдены</td></tr>';
return;
}
usersList.innerHTML = users.map(user => `
<tr>
<td class="py-3 px-4 font-medium text-slate-900 dark:text-white">${user.name}</td>
<td class="py-3 px-4 text-slate-600 dark:text-slate-400 hidden sm:table-cell text-sm">${user.email}</td>
<td class="py-3 px-4 text-slate-600 dark:text-slate-400">
<span class="px-2 py-1 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs font-medium">${this.getRoleName(user.role)}</span>
</td>
<td class="py-3 px-4">
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold ${user.active ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-400'}">
<i data-lucide="circle" class="w-2 h-2"></i>
${user.active ? 'Активен' : 'Неактивен'}
</span>
</td>
<td class="py-3 px-4 text-right">
<div class="flex items-center justify-end gap-2">
<button onclick="app.editUser(${user.id})" class="p-1 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded text-blue-600 dark:text-blue-400" title="Редактировать">
<i data-lucide="edit" class="w-4 h-4"></i>
</button>
<button onclick="app.deleteUser(${user.id})" class="p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded text-red-600 dark:text-red-400" title="Удалить">
<i data-lucide="trash" class="w-4 h-4"></i>
</button>
</div>
</td>
</tr>
`).join('');
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
this.setupAdminPanelHandlers();
} catch (error) {
console.error('Failed to load users:', error);
}
}
setupAdminPanelHandlers() {
// Add user button
const addUserBtn = document.getElementById('addUserBtn');
if (addUserBtn) {
addUserBtn.onclick = () => this.showUserModal(null);
}
// User modal controls
const userModal = document.getElementById('userModal');
const closeBtn = document.getElementById('closeUserModal');
const cancelBtn = document.getElementById('cancelUserEdit');
const userForm = document.getElementById('userForm');
if (closeBtn) closeBtn.onclick = () => userModal.classList.add('hidden');
if (cancelBtn) cancelBtn.onclick = () => userModal.classList.add('hidden');
if (userForm) {
userForm.onsubmit = (e) => this.handleUserFormSubmit(e);
}
}
showUserModal(userId) {
const userModal = document.getElementById('userModal');
const userForm = document.getElementById('userForm');
const userModalTitle = document.getElementById('userModalTitle');
userForm.reset();
userForm.dataset.userId = userId || '';
if (userId) {
userModalTitle.textContent = 'Редактировать пользователя';
// Load user data
// TODO: implement
} else {
userModalTitle.textContent = 'Добавить пользователя';
}
userModal.classList.remove('hidden');
}
async handleUserFormSubmit(e) {
e.preventDefault();
const formData = {
name: document.getElementById('userNameInput').value,
email: document.getElementById('userEmailInput').value,
role: document.getElementById('userRoleSelect').value,
password: document.getElementById('userPasswordInput').value
};
const userId = e.target.dataset.userId;
try {
if (userId) {
await api.updateUser(userId, formData);
} else {
await api.createUser(formData);
}
document.getElementById('userModal').classList.add('hidden');
this.loadUsers();
} catch (error) {
alert('Ошибка: ' + error.message);
}
}
async editUser(userId) {
// TODO: implement
this.showUserModal(userId);
}
async deleteUser(userId) {
if (confirm('Вы уверены, что хотите удалить этого пользователя?')) {
try {
await api.deleteUser(userId);
this.loadUsers();
} catch (error) {
alert('Ошибка: ' + error.message);
}
}
}
getRoleName(role) {
const names = {
'superadmin': 'Суперадминистратор',
'admin': 'Администратор',
'moderator': 'Модератор',
'viewer': 'Только просмотр'
};
return names[role] || role;
}
}
// Initialize application
const app = new Application();
// Update content when route changes
document.addEventListener('DOMContentLoaded', () => {
window.addEventListener('hashchange', () => {
const currentRoute = window.location.hash.slice(1);
// Load dashboard data
if (currentRoute.includes('dashboard') || currentRoute === '') {
app.loadDashboardData();
}
// Load admin panel
if (currentRoute.includes('admin')) {
app.loadAdminPanel();
}
});
});

212
public/js/auth.js Normal file
View File

@@ -0,0 +1,212 @@
// Authentication Handler
class Auth {
constructor() {
this.user = null;
this.isAuthenticated = false;
this.checkAuth();
}
async checkAuth() {
try {
const response = await api.getCurrentUser();
if (response.success) {
this.user = response.user;
this.isAuthenticated = true;
return true;
}
} catch (error) {
this.logout();
}
return false;
}
async login(email, password) {
try {
const response = await api.login(email, password);
if (response.success) {
this.user = response.user;
this.isAuthenticated = true;
return { success: true, user: this.user };
}
return { success: false, error: response.message };
} catch (error) {
return { success: false, error: error.message };
}
}
async register(name, email, password) {
try {
const response = await api.register(name, email, password);
if (response.success) {
return { success: true, message: 'Регистрация успешна. Теперь вы можете войти.' };
}
return { success: false, error: response.message };
} catch (error) {
return { success: false, error: error.message };
}
}
async logout() {
try {
await api.logout();
} catch (error) {
console.error('Logout error:', error);
}
this.user = null;
this.isAuthenticated = false;
window.location.hash = '#login';
}
hasRole(role) {
return this.user && this.user.role === role;
}
hasPermission(permission) {
if (!this.user) return false;
return this.user.permissions && this.user.permissions.includes(permission);
}
getUser() {
return this.user;
}
isAdmin() {
return this.user && (this.user.role === 'superadmin' || this.user.role === 'admin');
}
isSuperAdmin() {
return this.user && this.user.role === 'superadmin';
}
}
// Global auth instance
const auth = new Auth();
// Login/Register form handler
document.addEventListener('DOMContentLoaded', () => {
// Tab switching
const authTabBtns = document.querySelectorAll('.auth-tab-btn');
const authForms = document.querySelectorAll('.auth-form');
authTabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
// Update active tab
authTabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Show/hide forms
authForms.forEach(form => form.classList.add('hidden'));
if (tab === 'login') {
document.getElementById('loginForm').classList.remove('hidden');
} else {
document.getElementById('registerForm').classList.remove('hidden');
}
});
});
// Login form
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
const loginText = document.querySelector('.login-text');
const loginLoader = document.querySelector('.login-loader');
loginText.style.display = 'none';
loginLoader.classList.remove('hidden');
const result = await auth.login(email, password);
if (result.success) {
// Redirect to dashboard
setTimeout(() => {
window.location.hash = '#dashboard';
}, 500);
} else {
showAuthError(result.error);
loginText.style.display = 'block';
loginLoader.classList.add('hidden');
}
});
}
// Register form
const registerForm = document.getElementById('registerForm');
if (registerForm) {
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('registerName').value;
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
const confirmPassword = document.getElementById('registerPasswordConfirm').value;
if (password !== confirmPassword) {
showAuthError('Пароли не совпадают');
return;
}
const registerText = document.querySelector('.register-text');
const registerLoader = document.querySelector('.register-loader');
registerText.style.display = 'none';
registerLoader.classList.remove('hidden');
const result = await auth.register(name, email, password);
if (result.success) {
showAuthError(result.message, 'success');
// Clear form and switch to login
registerForm.reset();
document.querySelector('[data-tab="login"]').click();
} else {
showAuthError(result.error);
}
registerText.style.display = 'block';
registerLoader.classList.add('hidden');
});
}
});
function showAuthError(message, type = 'error') {
const errorDiv = document.getElementById('authError');
const errorText = document.getElementById('authErrorText');
if (type === 'success') {
errorDiv.classList.remove('bg-red-50', 'border-red-200', 'text-red-600', 'dark:bg-red-900/20', 'dark:border-red-800', 'dark:text-red-400');
errorDiv.classList.add('bg-green-50', 'border-green-200', 'text-green-600', 'dark:bg-green-900/20', 'dark:border-green-800', 'dark:text-green-400');
const icon = errorDiv.querySelector('i');
if (icon) {
icon.setAttribute('data-lucide', 'check-circle');
}
} else {
errorDiv.classList.remove('bg-green-50', 'border-green-200', 'text-green-600', 'dark:bg-green-900/20', 'dark:border-green-800', 'dark:text-green-400');
errorDiv.classList.add('bg-red-50', 'border-red-200', 'text-red-600', 'dark:bg-red-900/20', 'dark:border-red-800', 'dark:text-red-400');
const icon = errorDiv.querySelector('i');
if (icon) {
icon.setAttribute('data-lucide', 'alert-circle');
}
}
errorText.textContent = message;
errorDiv.classList.remove('hidden');
// Auto-hide success messages
if (type === 'success') {
setTimeout(() => {
errorDiv.classList.add('hidden');
}, 3000);
}
// Update Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}

205
public/js/router.js Normal file
View File

@@ -0,0 +1,205 @@
// Router - Single Page Application routing
class Router {
constructor() {
this.routes = {
'login': { module: 'authModule', handler: this.handleLogin.bind(this) },
'register': { module: 'authModule', handler: this.handleLogin.bind(this) },
'dashboard': { module: 'dashboardModule', requireAuth: true, handler: this.handleDashboard.bind(this) },
'databases': { module: 'dashboardModule', requireAuth: true, handler: this.handleDatabases.bind(this) },
'tables': { module: 'dashboardModule', requireAuth: true, handler: this.handleTables.bind(this) },
'queries': { module: 'dashboardModule', requireAuth: true, handler: this.handleQueries.bind(this) },
'admin-users': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminUsers.bind(this) },
'admin-roles': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminRoles.bind(this) },
'admin-settings': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminSettings.bind(this) },
'admin-logs': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminLogs.bind(this) },
'admin-database': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminDatabase.bind(this) },
'admin-backups': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminBackups.bind(this) },
};
this.currentRoute = null;
this.modules = {};
this.init();
}
async init() {
// Load modules
await this.loadModules();
// Setup hash change listener
window.addEventListener('hashchange', () => this.navigate());
// Initial navigation
this.navigate();
}
async loadModules() {
const modules = ['auth', 'dashboard', 'admin'];
for (const module of modules) {
try {
const response = await fetch(`/modules/${module}/${module === 'auth' ? 'login' : module === 'admin' ? 'admin-panel' : 'dashboard'}.html`);
if (response.ok) {
const html = await response.text();
const container = document.createElement('div');
container.innerHTML = html;
document.getElementById('app').appendChild(container);
}
} catch (error) {
console.error(`Failed to load ${module} module:`, error);
}
}
// Update Lucide icons after loading modules
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
navigate() {
const hash = window.location.hash.slice(1) || 'login';
const route = this.routes[hash];
if (!route) {
window.location.hash = '#login';
return;
}
// Check authentication
if (route.requireAuth && !auth.isAuthenticated) {
window.location.hash = '#login';
return;
}
// Check admin permission
if (route.requireAdmin && !auth.isAdmin()) {
window.location.hash = '#dashboard';
return;
}
// Hide all modules
document.querySelectorAll('[id$="Module"]').forEach(mod => {
mod.classList.add('hidden');
});
// Show target module
const module = document.getElementById(route.module);
if (module) {
module.classList.remove('hidden');
}
// Close sidebars on mobile when navigating
const sidebars = document.querySelectorAll('[id$="Sidebar"]');
sidebars.forEach(sidebar => {
sidebar.classList.add('hidden');
});
// Call route handler
if (route.handler) {
route.handler();
}
// Setup navigation event listeners for current route
this.setupNavigation();
// Update Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
this.currentRoute = hash;
}
setupNavigation() {
// Sidebar toggle
const toggleBtn = document.getElementById('toggleSidebar');
const sidebar = document.getElementById('sidebar');
if (toggleBtn && sidebar) {
toggleBtn.onclick = () => sidebar.classList.toggle('-translate-x-full');
}
// Admin sidebar toggle
const toggleAdminBtn = document.getElementById('toggleAdminSidebar');
const adminSidebar = document.getElementById('adminSidebar');
if (toggleAdminBtn && adminSidebar) {
toggleAdminBtn.onclick = () => adminSidebar.classList.toggle('-translate-x-full');
}
// Logout buttons
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.onclick = () => {
if (confirm('Вы уверены, что хотите выйти?')) {
auth.logout();
}
};
}
const adminLogoutBtn = document.getElementById('adminLogoutBtn');
if (adminLogoutBtn) {
adminLogoutBtn.onclick = () => {
if (confirm('Вы уверены, что хотите выйти?')) {
auth.logout();
}
};
}
// Update user info
if (auth.isAuthenticated && auth.user) {
const userName = document.getElementById('userName');
const userRole = document.getElementById('userRole');
const avatarCircle = document.getElementById('avatarCircle');
if (userName) userName.textContent = auth.user.name || 'User';
if (userRole) userRole.textContent = this.getRoleName(auth.user.role);
if (avatarCircle) avatarCircle.textContent = (auth.user.name || 'A').charAt(0).toUpperCase();
}
// Navigation items
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.classList.remove('active');
if (item.getAttribute('href') === `#${this.currentRoute}`) {
item.classList.add('active');
}
item.addEventListener('click', () => {
navItems.forEach(n => n.classList.remove('active'));
item.classList.add('active');
// Close sidebar on mobile
const sidebar = document.getElementById('sidebar') || document.getElementById('adminSidebar');
if (sidebar) {
sidebar.classList.add('-translate-x-full');
}
});
});
}
getRoleName(role) {
const names = {
'superadmin': 'Суперадминистратор',
'admin': 'Администратор',
'moderator': 'Модератор',
'viewer': 'Только просмотр'
};
return names[role] || role;
}
// Route handlers
handleLogin() { }
handleDashboard() { }
handleDatabases() { }
handleTables() { }
handleQueries() { }
handleAdminUsers() { }
handleAdminRoles() { }
handleAdminSettings() { }
handleAdminLogs() { }
handleAdminDatabase() { }
handleAdminBackups() { }
}
// Initialize router
const router = new Router();

76
public/js/theme.js Normal file
View File

@@ -0,0 +1,76 @@
// Theme Management
class ThemeManager {
constructor() {
this.storageKey = 'pgadmin-theme';
this.darkClass = 'dark';
this.init();
}
init() {
const savedTheme = localStorage.getItem(this.storageKey);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedTheme ? savedTheme === 'dark' : prefersDark;
if (isDark) {
this.setDark();
} else {
this.setLight();
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem(this.storageKey)) {
e.matches ? this.setDark() : this.setLight();
}
});
}
toggle() {
const html = document.documentElement;
if (html.classList.contains(this.darkClass)) {
this.setLight();
} else {
this.setDark();
}
}
setDark() {
document.documentElement.classList.add(this.darkClass);
localStorage.setItem(this.storageKey, 'dark');
this.updateIcons();
}
setLight() {
document.documentElement.classList.remove(this.darkClass);
localStorage.setItem(this.storageKey, 'light');
this.updateIcons();
}
isDark() {
return document.documentElement.classList.contains(this.darkClass);
}
updateIcons() {
// Update Lucide icons if needed
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
}
// Initialize theme manager
const themeManager = new ThemeManager();
// Theme toggle button handlers
document.addEventListener('DOMContentLoaded', () => {
const themeButtons = [
document.getElementById('toggleTheme'),
document.getElementById('toggleAdminTheme')
];
themeButtons.forEach(btn => {
if (btn) {
btn.addEventListener('click', () => themeManager.toggle());
}
});
});