USER
раздили на бэк и фронт мой код и напиши структуру
// app.js
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const { GPU } = require('gpu.js');
const session = require('express-session'); // Added for session management
const bcrypt = require('bcrypt'); // Added for password hashing
const { v4: uuidv4 } = require('uuid'); // Added for token generation
// Инициализация GPU.js
const gpu = new GPU();
// Константы для путей и параметров
const PORT = 3000;
const BD_DIR = path.join(__dirname, 'BD');
const USERS_FILE = path.join(BD_DIR, 'users.json');
const SALT_ROUNDS = 10;
// Убедимся, что папка BD существует
if (!fs.existsSync(BD_DIR)) {
fs.mkdirSync(BD_DIR);
}
// Убедимся, что файл users.json существует
if (!fs.existsSync(USERS_FILE)) {
fs.writeFileSync(USERS_FILE, JSON.stringify([]), 'utf8');
}
// Чтение пользователей из файла
let users = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
// Класс Table для управления таблицами
class Table {
constructor(name, schema, autoIncrement = false) {
this.name = name;
this.schema = schema; // Объект с типами, например: { id: 'number', name: 'string' }
this.autoIncrement = autoIncrement; // Флаг автоинкремента для 'id'
this.autoIncrementValue = 1; // Начальное значение для автоинкремента
// Инициализация колонок
this.columns = {};
for (let field in schema) {
if (schema[field] === 'number') {
this.columns[field] = []; // Массив чисел
} else {
this.columns[field] = []; // Массив строк
}
}
}
// Метод для вставки записи
insert(record) {
// Если автоинкремент включен, генерируем id
if (this.autoIncrement) {
record.id = this.autoIncrementValue++;
} else {
if (record.id === undefined || record.id === null) {
throw new Error('Поле "id" обязательно для вставки записи.');
}
const id = record.id;
const ids = this.columns['id'];
if (ids.includes(id)) {
throw new Error(`Запись с id=${id} уже существует.`);
}
}
for (let field in this.schema) {
if (this.autoIncrement && field === 'id') {
// Уже установили значение id
this.columns[field].push(record[field]);
} else {
this.columns[field].push(
record[field] !== undefined ? record[field] : (this.schema[field] === 'number' ? 0 : '')
);
}
}
}
// Метод для удаления записи по id
deleteRecord(id) {
const ids = this.columns['id'];
const index = ids.indexOf(id);
if (index !== -1) {
for (let field in this.columns) {
this.columns[field].splice(index, 1);
}
return true;
}
return false;
}
// Метод для обновления записи по id
updateRecord(id, newRecord) {
const ids = this.columns['id'];
const index = ids.indexOf(id);
if (index !== -1) {
for (let field in newRecord) {
if (this.schema[field]) {
if (field === 'id' && this.autoIncrement) {
// Не разрешаем изменять auto-increment id вручную
continue;
}
this.columns[field][index] = newRecord[field];
}
}
return true;
}
return false;
}
// Метод для получения записи по id с использованием GPU
getGPU(id) {
// Преобразуем колонки в массивы чисел или строк
const recordLength = this.columns['id'].length;
const ids = this.columns['id'];
// Создаем TypedArray для id, если они числа
let idArray;
if (this.schema['id'] === 'number') {
idArray = new Float32Array(ids);
} else {
// Для строк GPU.js не поддерживает, поэтому ограничимся числовым id.
throw new Error('GPU-поиск поддерживается только для числовых id.');
}
// Создаем GPU Kernel для поиска индекса
const findIndexGPU = gpu.createKernel(function(idArray, targetId) {
let index = -1;
for (let i = 0; i < this.constants.size; i++) {
if (idArray[i] === targetId) {
index = i;
break;
}
}
return index;
})
.setOutput([1])
.setConstants({ size: recordLength });
const index = findIndexGPU(idArray, id)[0];
if (index === -1) return null;
// Получение всей записи
const record = {};
for (let field in this.columns) {
record[field] = this.columns[field][index];
}
return record;
}
getRecord(id) {
return this.getGPU(id);
// Или использовать CPU метод:
// return this.getCPU(id);
}
getAll() {
// Преобразование колонночного хранения в массив записей
const recordLength = this.columns['id'].length;
const records = [];
for (let i = 0; i < recordLength; i++) {
const record = {};
for (let field in this.columns) {
record[field] = this.columns[field][i];
}
records.push(record);
}
return records;
}
// Использование CPU для получения записи по id
getCPU(id) {
const index = this.columns['id'].indexOf(id);
if (index === -1) return null;
const record = {};
for (let field in this.columns) {
record[field] = this.columns[field][index];
}
return record;
}
// Добавление новой колонки
addColumn(columnName, columnType) {
if (this.schema[columnName]) {
throw new Error(`Колонка "${columnName}" уже существует.`);
}
if (!['number', 'string'].includes(columnType.toLowerCase())) {
throw new Error('Поддерживаются только типы "number" и "string".');
}
this.schema[columnName] = columnType.toLowerCase();
// Инициализация новой колонки
this.columns[columnName] = this.columns['id'].map(() => (columnType.toLowerCase() === 'number' ? 0 : ''));
}
// Удаление колонки
deleteColumn(columnName) {
if (!this.schema[columnName]) {
throw new Error(`Колонка "${columnName}" не найдена.`);
}
if (columnName === 'id') {
throw new Error('Невозможно удалить обязательную колонку "id".');
}
delete this.schema[columnName];
delete this.columns[columnName];
}
}
// Класс Database с автоматическим сохранением после каждой модификации
class Database {
constructor(filePath) {
this.tables = new Map();
this.filePath = filePath; // Сохраняем путь к файлу
}
createTable(tableName, schema, autoIncrement = false) {
if (this.tables.has(tableName)) {
throw new Error(`Таблица "${tableName}" уже существует.`);
}
const table = new Table(tableName, schema, autoIncrement);
// Если автоинкремент включен, устанавливаем начальное значение
if (autoIncrement) {
const ids = table.columns['id'];
if (ids.length > 0) {
table.autoIncrementValue = Math.max(...ids) + 1;
} else {
table.autoIncrementValue = 1;
}
}
this.tables.set(tableName, table);
this.saveToFile(); // Сохранение после создания таблицы
}
dropTable(tableName) {
if (this.tables.delete(tableName)) {
this.saveToFile(); // Сохранение после удаления таблицы
return true;
}
return false;
}
insert(tableName, record) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
table.insert(record);
this.saveToFile(); // Сохранение после вставки записи
}
deleteRecord(tableName, id) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
const result = table.deleteRecord(id);
if (result) {
this.saveToFile(); // Сохранение после удаления записи
}
return result;
}
updateRecord(tableName, id, newRecord) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
const result = table.updateRecord(id, newRecord);
if (result) {
this.saveToFile(); // Сохранение после обновления записи
}
return result;
}
getRecord(tableName, id) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
return table.getRecord(id);
}
getAll(tableName) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
return table.getAll();
}
getTables() {
return Array.from(this.tables.keys());
}
getTableSchema(tableName) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
return table.schema;
}
getTableAutoIncrement(tableName) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
return table.autoIncrement;
}
addColumn(tableName, columnName, columnType) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
table.addColumn(columnName, columnType);
this.saveToFile(); // Сохранение после добавления колонки
}
deleteColumn(tableName, columnName) {
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
table.deleteColumn(columnName);
this.saveToFile(); // Сохранение после удаления колонки
}
// Сохранение базы данных в файл
saveToFile() {
const data = {};
this.tables.forEach((table, name) => {
data[name] = {
schema: table.schema,
columns: table.columns,
autoIncrement: table.autoIncrement,
autoIncrementValue: table.autoIncrementValue
};
});
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8');
console.log(`Данные сохранены в ${this.filePath}`);
}
// Загрузка базы данных из файла
loadFromFile() {
if (fs.existsSync(this.filePath)) {
const rawData = fs.readFileSync(this.filePath, 'utf8');
const data = JSON.parse(rawData);
for (const tableName in data) {
const tableData = data[tableName];
const table = new Table(tableName, tableData.schema, tableData.autoIncrement);
for (let field in tableData.columns) {
table.columns[field] = tableData.columns[field];
}
table.autoIncrementValue = tableData.autoIncrementValue || 1;
this.tables.set(tableName, table);
}
console.log(`Данные загружены из ${this.filePath}`);
} else {
console.log(`Файл ${this.filePath} не найден. Начинается с пустой базы данных.`);
}
}
// Метод для выполнения SQL-подобной команды
executeSQLCommand(command) {
const parsed = parseSQL(command);
if (!parsed) {
throw new Error('Некорректная команда.');
}
const { type } = parsed;
switch (type.toUpperCase()) {
case 'SELECT':
if (parsed.type === 'JOIN') { // Изменено
return this.selectQuery(parsed.table1, parsed.fields, parsed.where, parsed.order, parsed); // Изменено
}
return this.selectQuery(parsed.tableName, parsed.fields, parsed.where, parsed.order);
case 'INSERT':
return this.insertQuery(parsed.tableName, parsed.values);
case 'UPDATE':
return this.updateQuery(parsed.tableName, parsed.fields, parsed.where);
case 'DELETE':
return this.deleteQuery(parsed.tableName, parsed.where);
default:
throw new Error(`Неизвестный тип команды "${type}".`);
}
}
// Методы для поддержки SQL-команд
selectQuery(tableName, selectedFields = ['*'], where = null, order = null, join = null) { // Изменено
const table = this.tables.get(tableName);
if (!table) {
throw new Error(`Таблица "${tableName}" не найдена.`);
}
let records = table.getAll();
// Если есть JOIN
if (join) { // Добавлено
const joinTable = this.tables.get(join.table2);
if (!joinTable) {
throw new Error(`Таблица для JOIN "${join.table2}" не найдена.`);
}
const [leftTableName, leftField] = join.joinCondition.left.split('.');
const [rightTableName, rightField] = join.joinCondition.right.split('.');
if (leftTableName !== tableName || rightTableName !== join.table2) {
throw new Error('Некорректные условия JOIN.');
}
const joinRecords = joinTable.getAll();
// Выполнение INNER JOIN
records = records.flatMap(record1 => {
return joinRecords.filter(record2 => record1[leftField] === record2[rightField])
.map(record2 => ({ ...record1, ...record2 }));
});
}
// Применение WHERE
if (where) {
records = records.filter(record => evaluateWhere(record, where));
}
// Применение ORDER BY
if (order) {
const { field, direction } = order;
if (!Object.keys(records[0] || {}).includes(field)) {
throw new Error(`Поле "${field}" не найдено в результирующем наборе данных.`);
}
records.sort((a, b) => {
if (a[field] < b[field]) return direction === 'ASC' ? -1 : 1;
if (a[field] > b[field]) return direction === 'ASC' ? 1 : -1;
return 0;
});
}
// Выбор полей
if (!(selectedFields.length === 1 && selectedFields[0] === '*')) {
records = records.map(record => {
const selected = {};
selectedFields.forEach(field => {
if (!(field in record)) {
throw new Error(`Поле "${field}" не найдено в таблицах.`);
}
selected[field] = record[field];
});
return selected;
});
}
return records;
}
// Остальные методы остаются без изменений
}
// Функции для парсинга и оценки WHERE
function parseSQL(command) {
const selectRegex = /^SELECT\s+(.+)\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(\w+)\s+(ASC|DESC))?;?$/i;
const insertRegex = /^INSERT\s+INTO\s+(\w+)\s*\(([^)]+)\)\s+VALUES\s*\(([^)]+)\);?$/i;
const updateRegex = /^UPDATE\s+(\w+)\s+SET\s+([^;]+?)(?:\s+WHERE\s+(.+))?;?$/i;
const deleteRegex = /^DELETE\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+))?;?$/i;
// Новое регулярное выражение для JOIN
const joinRegex = /^SELECT\s+(.+)\s+FROM\s+(\w+)\s+JOIN\s+(\w+)\s+ON\s+(\w+\.\w+)\s*=\s*(\w+\.\w+)(?:\s+WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(\w+)\s+(ASC|DESC))?;?$/i;
let match = command.match(joinRegex);
if (match) {
const fields = match[1].split(',').map(f => f.trim());
const table1 = match[2];
const table2 = match[3];
const joinConditionLeft = match[4];
const joinConditionRight = match[5];
const where = match[6] ? parseWhere(match[6]) : null;
const orderField = match[7];
const orderDirection = match[8] ? match[8].toUpperCase() : 'ASC';
const order = orderField ? { field: orderField, direction: orderDirection } : null;
return {
type: 'JOIN', // Обновлено
fields,
table1,
table2,
joinCondition: { left: joinConditionLeft, right: joinConditionRight }, // Добавлено
where,
order
};
}
let matchSelect = command.match(selectRegex);
if (matchSelect) {
const fields = matchSelect[1].split(',').map(f => f.trim());
const tableName = matchSelect[2];
const where = matchSelect[3] ? parseWhere(matchSelect[3]) : null;
const orderField = matchSelect[4];
const orderDirection = matchSelect[5] ? matchSelect[5].toUpperCase() : 'ASC';
const order = orderField ? { field: orderField, direction: orderDirection } : null;
return { type: 'SELECT', tableName, fields, where, order };
}
let matchInsert = command.match(insertRegex);
if (matchInsert) {
const tableName = matchInsert[1];
const fields = matchInsert[2].split(',').map(f => f.trim());
const values = matchInsert[3].split(',').map(v => {
v = v.trim();
if (v.startsWith('"') && v.endsWith('"')) {
return v.slice(1, -1);
}
if (v.startsWith("'") && v.endsWith("'")) {
return v.slice(1, -1);
}
return Number(v);
});
const record = {};
fields.forEach((field, index) => {
record[field] = values[index];
});
return { type: 'INSERT', tableName, values: record };
}
let matchUpdate = command.match(updateRegex);
if (matchUpdate) {
const tableName = matchUpdate[1];
const setClauses = matchUpdate[2].split(',').map(c => c.trim());
const fields = {};
setClauses.forEach(clause => {
const [field, value] = clause.split('=').map(s => s.trim());
let processedValue = value;
if (value.startsWith('"') && value.endsWith('"')) {
processedValue = value.slice(1, -1);
}
if (value.startsWith("'") && value.endsWith("'")) {
processedValue = value.slice(1, -1);
}
if (!isNaN(Number(processedValue))) {
processedValue = Number(processedValue);
}
fields[field] = processedValue;
});
const where = matchUpdate[3] ? parseWhere(matchUpdate[3]) : null;
return { type: 'UPDATE', tableName, fields, where };
}
let matchDelete = command.match(deleteRegex);
if (matchDelete) {
const tableName = matchDelete[1];
const where = matchDelete[2] ? parseWhere(matchDelete[2]) : null;
return { type: 'DELETE', tableName, where };
}
return null;
}
function parseWhere(whereClause) {
// Поддержка простых условий вида "field operator value"
const operators = ['>=', '<=', '!=', '=', '>', '<'];
for (let op of operators) {
const parts = whereClause.split(op);
if (parts.length === 2) {
const field = parts[0].trim();
let value = parts[1].trim();
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
if (!isNaN(Number(value))) {
value = Number(value);
}
return { field, operator: op, value };
}
}
return null;
}
function evaluateWhere(record, where) {
const { field, operator, value } = where;
const recordValue = record[field];
switch (operator) {
case '=':
return recordValue === value;
case '!=':
return recordValue !== value;
case '>':
return recordValue > value;
case '<':
return recordValue < value;
case '>=':
return recordValue >= value;
case '<=':
return recordValue <= value;
default:
return false;
}
}
// Инициализация приложения
const app = express();
// Настройка промежуточного ПО
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json()); // Для обработки JSON тел запросов
app.use(express.static(path.join(__dirname, 'public')));
// Настройка сессий
app.use(session({
secret: 'your-secret-key', // Замените на ваш секрет
resave: false,
saveUninitialized: false
}));
// Функция для рендеринга HTML страниц с добавлением стилей и анимаций
function renderPage(res, title, content) {
const html = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>${title}</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<!-- Animate.css for animations -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<style>
body {
padding-top: 70px;
background-color: #f0f2f5;
font-family: 'Roboto', sans-serif;
}
.table-actions {
display: flex;
gap: 5px;
}
/* Стиль для консоли */
#command-console {
width: 100%;
height: 200px;
}
#command-result {
white-space: pre-wrap;
background-color: #ffffff;
padding: 15px;
border: 1px solid #ced4da;
border-radius: 4px;
height: 200px;
overflow-y: scroll;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
/* Кастомные стили */
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
animation: fadeInDown 1s;
}
table {
animation: fadeIn 1s;
}
.btn-primary {
transition: background-color 0.3s, transform 0.3s;
}
.btn-primary:hover {
background-color: #0056b3;
transform: scale(1.05);
}
.btn-success, .btn-danger, .btn-secondary {
transition: transform 0.3s;
}
.btn-success:hover, .btn-danger:hover, .btn-secondary:hover {
transform: scale(1.05);
}
/* Анимации для содержимого контейнера */
.container {
animation: fadeInUp 1s;
}
/* Ховеры для строк таблицы */
table tbody tr:hover {
background-color: #f1f1f1;
transition: background-color 0.3s;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top animate__animated animate__fadeInDown">
<a class="navbar-brand" href="/">Custom DB Manager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
${title !== 'Вход' && title !== 'Регистрация' ? `
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item ${title === 'Список Таблиц' ? 'active' : ''}">
<a class="nav-link" href="/">Таблицы</a>
</li>
<li class="nav-item ${title === 'Создать Таблицу' ? 'active' : ''}">
<a class="nav-link" href="/create-table">Создать Таблицу</a>
</li>
<li class="nav-item ${title === 'Выполнить Команду' ? 'active' : ''}">
<a class="nav-link" href="/execute-command">Выполнить Команду</a>
</li>
</ul>
<span class="navbar-text mr-3">
Привет, ${app.locals.currentUser || 'Гость'}!
</span>
<a href="/logout" class="btn btn-outline-light">Выйти</a>
</div>
` : ''}
</nav>
<div class="container mt-4 animate__animated animate__fadeInUp">
${content}
</div>
<!-- Bootstrap JS и зависимости -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<!-- Дополнительные скрипты для анимаций -->
<script>
// Пример анимации при загрузке страницы
document.addEventListener('DOMContentLoaded', () => {
const elements = document.querySelectorAll('.animate-on-load');
elements.forEach(el => {
el.classList.add('animate__animated', 'animate__fadeIn');
});
});
</script>
</body>
</html>
`;
res.send(html);
}
// -----------------------------------------------------------------------------------
// Функции управления пользователями
// -----------------------------------------------------------------------------------
// Функция сохранения пользователей в файл
function saveUsers() {
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2), 'utf8');
console.log(`Пользователи сохранены в ${USERS_FILE}`);
}
// Функция для поиска пользователя по имени
function findUser(username) {
return users.find(user => user.username === username);
}
// Функция для поиска пользователя по токену
function findUserByToken(token) {
return users.find(user => user.token === token);
}
// -----------------------------------------------------------------------------------
// Маршруты аутентификации
// -----------------------------------------------------------------------------------
// Страница регистрации
app.get('/register', (req, res) => {
const content = `
<h1 class="animate__animated animate__fadeInDown">Регистрация</h1>
<form action="/register" method="POST" class="animate__animated animate__fadeIn">
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Введите имя пользователя" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Введите пароль" required>
</div>
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Зарегистрироваться</button>
<a href="/login" class="btn btn-secondary animate__animated animate__fadeIn">Войти</a>
</form>
`;
renderPage(res, 'Регистрация', content);
});
// Обработка регистрации
app.post('/register', async (req, res) => {
const { username, password } = req.body;
try {
// Проверка наличия пользователя
if (findUser(username)) {
throw new Error('Имя пользователя уже занято.');
}
// Хеширование пароля
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
// Генерация уникального токена
const token = uuidv4();
// Создание пользователя
const newUser = { username, passwordHash, token };
users.push(newUser);
saveUsers();
// Создание папки пользователя
const userDir = path.join(BD_DIR, username);
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir);
}
// Инициализация базы данных пользователя
const userDBPath = path.join(userDir, 'database.json');
if (!fs.existsSync(userDBPath)) {
const emptyDB = { tables: {} };
fs.writeFileSync(userDBPath, JSON.stringify(emptyDB, null, 2), 'utf8');
}
// Перенаправление на страницу входа
res.redirect('/login');
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка регистрации</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/register" class="btn btn-primary animate__animated animate__fadeIn">Попробовать снова</a>
</div>
`;
renderPage(res, 'Ошибка регистрации', content);
}
});
// Страница входа
app.get('/login', (req, res) => {
const content = `
<h1 class="animate__animated animate__fadeInDown">Вход</h1>
<form action="/login" method="POST" class="animate__animated animate__fadeIn">
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Введите имя пользователя" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Введите пароль" required>
</div>
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Войти</button>
<a href="/register" class="btn btn-secondary animate__animated animate__fadeIn">Регистрация</a>
</form>
`;
renderPage(res, 'Вход', content);
});
// Обработка входа
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const user = findUser(username);
if (!user) {
throw new Error('Неверное имя пользователя или пароль.');
}
const passwordMatch = await bcrypt.compare(password, user.passwordHash);
if (!passwordMatch) {
throw new Error('Неверное имя пользователя или пароль.');
}
// Установка сессии
req.session.username = username;
res.redirect('/');
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка входа</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/login" class="btn btn-primary animate__animated animate__fadeIn">Попробовать снова</a>
</div>
`;
renderPage(res, 'Ошибка входа', content);
}
});
// Обработка выхода
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.redirect('/login');
});
});
// -----------------------------------------------------------------------------------
// Middleware для защиты маршрутов
// -----------------------------------------------------------------------------------
function isAuthenticated(req, res, next) {
if (req.session.username) {
app.locals.currentUser = req.session.username; // Для отображения в навбаре
return next();
}
res.redirect('/login');
}
// -----------------------------------------------------------------------------------
// Веб-интерфейс (существующий код, теперь защищенный аутентификацией)
// -----------------------------------------------------------------------------------
// Главная страница - список таблиц
app.get('/', isAuthenticated, (req, res) => {
const username = req.session.username;
const userDBPath = path.join(BD_DIR, username, 'database.json');
// Инициализация базы данных пользователя
const db = new Database(userDBPath);
db.loadFromFile();
const tables = db.getTables();
let content = `<h1 class="animate__animated animate__fadeInDown">Список Таблиц</h1>`;
// Добавление раздела для отображения и управления токеном
const user = findUser(username);
content += `
<script>
function copyToken() {
const tokenInput = document.querySelector('input[readonly]');
tokenInput.select();
tokenInput.setSelectionRange(0, 99999); // Для мобильных устройств
document.execCommand('copy');
alert('Токен скопирован в буфер обмена.');
}
function regenerateToken() {
if (confirm('Вы уверены, что хотите сгенерировать новый токен? Старый токен станет недействительным.')) {
fetch('/regenerate-token', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Новый токен сгенерирован.');
location.reload();
} else {
alert('Ошибка: ' + data.message);
}
})
.catch(err => {
alert('Произошла ошибка.');
});
}
}
</script>
`;
if (tables.length === 0) {
content += `<div class="alert alert-info animate__animated animate__fadeIn">Нет созданных таблиц. Создайте первую таблицу <a href="/create-table">здесь</a>.</div>`;
} else {
content += `
<table class="table table-bordered table-hover">
<thead class="thead-dark">
<tr>
<th>Название Таблицы</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
`;
tables.forEach(table => {
content += `
<tr>
<td><a href="/table/${encodeURIComponent(table)}" class="animate__animated animate__fadeInUp">${table}</a></td>
<td>
<div class="table-actions">
<a href="/table/${encodeURIComponent(table)}/manage-schema" class="btn btn-secondary btn-sm animate__animated animate__fadeIn">Управление Структурой</a>
<form action="/drop-table/${encodeURIComponent(table)}" method="POST" onsubmit="return confirm('Вы уверены, что хотите удалить таблицу ${table}?');" class="mb-0">
<button type="submit" class="btn btn-danger btn-sm animate__animated animate__fadeIn">Удалить</button>
</form>
</div>
</td>
</tr>
`;
});
content += `
</tbody>
</table>
`;
}
renderPage(res, 'Список Таблиц', content);
});
// Обработка запроса на регенерацию токена
app.post('/regenerate-token', isAuthenticated, (req, res) => {
const username = req.session.username;
const user = findUser(username);
if (!user) {
return res.status(400).json({ success: false, message: 'Пользователь не найден.' });
}
user.token = uuidv4();
saveUsers();
res.json({ success: true });
});
// Страница регистрации новой таблицы
app.get('/create-table', isAuthenticated, (req, res) => {
const content = `
<h1 class="animate__animated animate__fadeInDown">Создать Новую Таблицу</h1>
<form action="/create-table" method="POST" class="animate__animated animate__fadeIn">
<div class="form-group">
<label for="tableName">Название Таблицы</label>
<input type="text" class="form-control" id="tableName" name="tableName" placeholder="Введите название таблицы" required>
</div>
<div class="form-group">
<label for="columns">Поля таблицы</label>
<div id="columns-container">
<div class="form-row mb-2 animate__animated animate__fadeIn">
<div class="col-md-4">
<input type="text" class="form-control" name="columnNames" placeholder="Имя колонки" required>
</div>
<div class="col-md-4">
<select class="form-control" name="columnTypes" required>
<option value="" disabled selected>Выберите тип</option>
<option value="number">Number</option>
<option value="string">String</option>
</select>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-danger remove-column animate__animated animate__fadeIn">×</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-primary" id="add-column">Добавить Колонку</button>
<small class="form-text text-muted">Минимум одно поле должно быть "id: number".</small>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="autoIncrement" name="autoIncrement">
<label class="form-check-label" for="autoIncrement">Автоинкремент для поля "id"</label>
</div>
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Создать Таблицу</button>
<a href="/" class="btn btn-secondary animate__animated animate__fadeIn">Отмена</a>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const addColumnButton = document.getElementById('add-column');
const columnsContainer = document.getElementById('columns-container');
addColumnButton.addEventListener('click', function() {
const newColumn = document.createElement('div');
newColumn.className = 'form-row mb-2 animate__animated animate__fadeIn';
newColumn.innerHTML = \`
<div class="col-md-4">
<input type="text" class="form-control" name="columnNames" placeholder="Имя колонки" required>
</div>
<div class="col-md-4">
<select class="form-control" name="columnTypes" required>
<option value="" disabled selected>Выберите тип</option>
<option value="number">Number</option>
<option value="string">String</option>
</select>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-danger remove-column animate__animated animate__fadeIn">×</button>
</div>
\`;
columnsContainer.appendChild(newColumn);
});
columnsContainer.addEventListener('click', function(e) {
if (e.target && e.target.classList.contains('remove-column')) {
const row = e.target.closest('.form-row');
row.classList.add('animate__animated', 'animate__fadeOut');
row.addEventListener('animationend', function() {
row.remove();
});
}
});
});
</script>
`;
renderPage(res, 'Создать Таблицу', content);
});
// Обработка создания новой таблицы
app.post('/create-table', isAuthenticated, (req, res) => {
const username = req.session.username;
const userDBPath = path.join(BD_DIR, username, 'database.json');
const { tableName, autoIncrement } = req.body;
// Получение массивов имен и типов колонок
let columnNames = req.body.columnNames;
let columnTypes = req.body.columnTypes;
// Обеспечение того, что columnNames и columnTypes являются массивами
if (!Array.isArray(columnNames)) {
columnNames = [columnNames];
}
if (!Array.isArray(columnTypes)) {
columnTypes = [columnTypes];
}
try {
// Парсинг колонок
const schema = {};
for (let i = 0; i < columnNames.length; i++) {
const name = columnNames[i].trim();
const type = columnTypes[i].trim().toLowerCase();
if (!name) {
throw new Error(`Имя колонки в строке ${i + 1} не может быть пустым.`);
}
if (!['number', 'string'].includes(type)) {
throw new Error(`Некорректный тип колонки "${name}" в строке ${i + 1}. Допустимые типы: number, string.`);
}
if (schema[name]) {
throw new Error(`Дублирующееся имя колонки: "${name}" в строке ${i + 1}.`);
}
schema[name] = type;
}
// Проверка наличия обязательной колонки 'id'
if (!schema.id) {
throw new Error('Необходимо указать колонку "id: number" в качестве уникального идентификатора.');
}
if (schema.id !== 'number') {
throw new Error('Колонка "id" должна быть типа "number".');
}
// Проверка на уникальность имен колонок
const uniqueColumns = new Set(Object.keys(schema));
if (uniqueColumns.size !== Object.keys(schema).length) {
throw new Error('Имена колонок должны быть уникальными.');
}
// Проверка автоинкремента: если включен, убедимся, что 'id' присутствует и числовой
const isAutoIncrement = autoIncrement === 'on';
// Инициализация базы данных пользователя
const db = new Database(userDBPath);
db.loadFromFile();
db.createTable(tableName, schema, isAutoIncrement);
res.redirect('/');
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/create-table" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Страница управления структурой таблицы
app.get('/table/:tableName/manage-schema', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const schema = db.getTableSchema(tableName);
const autoIncrement = db.getTableAutoIncrement(tableName);
let content = `<h1 class="animate__animated animate__fadeInDown">Управление Структурой Таблицы: ${tableName}</h1>`;
// Форма добавления новой колонки
content += `
<h3 class="animate__animated animate__fadeInUp">Добавить Новую Колонку</h3>
<form action="/table/${encodeURIComponent(tableName)}/add-column" method="POST" class="animate__animated animate__fadeIn">
<div class="form-row">
<div class="col-md-5 mb-3">
<label for="columnName">Название Колонки</label>
<input type="text" class="form-control" id="columnName" name="columnName" placeholder="Например: email" required>
</div>
<div class="col-md-5 mb-3">
<label for="columnType">Тип Колонки</label>
<select class="form-control" id="columnType" name="columnType" required>
<option value="" disabled selected>Выберите тип</option>
<option value="number">Number</option>
<option value="string">String</option>
</select>
</div>
<div class="col-md-2 mb-3 align-self-end">
<button type="submit" class="btn btn-success animate__animated animate__fadeIn">Добавить</button>
</div>
</div>
</form>
`;
// Таблица текущей схемы
content += `
<h3 class="animate__animated animate__fadeInUp">Текущая Структура Таблицы</h3>
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th>Название Колонки</th>
<th>Тип</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
`;
for (let field in schema) {
content += `
<tr>
<td>${field}</td>
<td>${schema[field]}</td>
<td>
${field !== 'id' ? `
<form action="/table/${encodeURIComponent(tableName)}/delete-column" method="POST" onsubmit="return confirm('Вы уверены, что хотите удалить колонку "${field}"? Это приведет к потере всех данных в этой колонке.');" class="mb-0 animate__animated animate__fadeIn">
<input type="hidden" name="columnName" value="${field}">
<button type="submit" class="btn btn-danger btn-sm">Удалить</button>
</form>
` : `
<span class="text-muted">Обязательная колонка ${autoIncrement ? '(Автоинкремент)' : ''}</span>
`}
</td>
</tr>
`;
}
content += `
</tbody>
</table>
<a href="/table/${encodeURIComponent(tableName)}" class="btn btn-secondary animate__animated animate__fadeIn">Назад</a>
`;
renderPage(res, `Управление Структурой ${tableName}`, content);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Обработка добавления новой колонки
app.post('/table/:tableName/add-column', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const { columnName, columnType } = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
if (columnName.toLowerCase() === 'id') {
throw new Error('Невозможно добавить колонку с именем "id". Оно зарезервировано.');
}
const db = new Database(userDBPath);
db.loadFromFile();
db.addColumn(tableName, columnName, columnType);
res.redirect(`/table/${encodeURIComponent(tableName)}/manage-schema`);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/table/${encodeURIComponent(tableName)}/manage-schema" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Обработка удаления колонки
app.post('/table/:tableName/delete-column', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const { columnName } = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
db.deleteColumn(tableName, columnName);
res.redirect(`/table/${encodeURIComponent(tableName)}/manage-schema`);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/table/${encodeURIComponent(tableName)}/manage-schema" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Страница отображения таблицы и ее записей
app.get('/table/:tableName', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const records = db.getAll(tableName);
const schema = db.getTableSchema(tableName);
const autoIncrement = db.getTableAutoIncrement(tableName);
let content = `<h1 class="animate__animated animate__fadeInDown">Таблица: ${tableName}</h1>`;
content += `
<div class="mb-3">
<a href="/table/${encodeURIComponent(tableName)}/add" class="btn btn-success animate__animated animate__fadeIn">Добавить Запись</a>
<a href="/table/${encodeURIComponent(tableName)}/manage-schema" class="btn btn-secondary animate__animated animate__fadeIn">Управление Структурой</a>
</div>
`;
content += `
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead class="thead-dark">
<tr>
`;
for (let field in schema) {
content += `<th>${field}</th>`;
}
content += `<th>Действия</th></tr></thead><tbody>`;
if (records.length === 0) {
content += `
<tr>
<td colspan="${Object.keys(schema).length + 1}" class="text-center">Нет записей.</td>
</tr>
`;
} else {
records.forEach(record => {
content += `<tr class="animate__animated animate__fadeInUp">`;
for (let field in schema) {
content += `<td>${record[field]}</td>`;
}
content += `
<td>
<div class="table-actions">
<a href="/table/${encodeURIComponent(tableName)}/edit/${encodeURIComponent(record.id)}" class="btn btn-primary btn-sm animate__animated animate__fadeIn">Редактировать</a>
<form action="/table/${encodeURIComponent(tableName)}/delete/${encodeURIComponent(record.id)}" method="POST" onsubmit="return confirm('Вы уверены, что хотите удалить запись с id=${record.id}?');" class="mb-0 animate__animated animate__fadeIn">
<button type="submit" class="btn btn-danger btn-sm">Удалить</button>
</form>
</div>
</td>
`;
content += `</tr>`;
});
}
content += `</tbody></table></div>`;
content += `<a href="/" class="btn btn-secondary animate__animated animate__fadeIn">Назад</a>`;
renderPage(res, `Таблица: ${tableName}`, content);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Страница добавления новой записи
app.get('/table/:tableName/add', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const schema = db.getTableSchema(tableName);
const autoIncrement = db.getTableAutoIncrement(tableName);
let formFields = '';
for (let field in schema) {
if (field === 'id' && autoIncrement) {
// Если автоинкремент, не отображаем поле id
continue;
}
formFields += `
<div class="form-group animate__animated animate__fadeIn">
<label for="${field}">${field} (${schema[field]})</label>
${schema[field] === 'number' ? `
<input type="number" class="form-control" id="${field}" name="${field}" required>
` : `
<input type="text" class="form-control" id="${field}" name="${field}" required>
`}
</div>
`;
}
const content = `
<h1 class="animate__animated animate__fadeInDown">Добавить Запись в Таблицу: ${tableName}</h1>
<form action="/table/${encodeURIComponent(tableName)}/add" method="POST" class="animate__animated animate__fadeIn">
${formFields}
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Добавить Запись</button>
<a href="/table/${encodeURIComponent(tableName)}" class="btn btn-secondary animate__animated animate__fadeIn">Отмена</a>
</form>
`;
renderPage(res, `Добавить Запись в ${tableName}`, content);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Обработка добавления новой записи
app.post('/table/:tableName/add', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const record = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const schema = db.getTableSchema(tableName);
const autoIncrement = db.getTableAutoIncrement(tableName);
// Если автоинкремент для 'id', удаляем ручной ввод 'id' (если он был)
if (autoIncrement && record.id !== undefined) {
delete record.id;
}
// Преобразование типов полей
for (const field in record) {
if (schema[field] === 'number') {
record[field] = Number(record[field]);
if (isNaN(record[field])) {
throw new Error(`Поле "${field}" должно быть числом.`);
}
}
}
db.insert(tableName, record);
res.redirect(`/table/${encodeURIComponent(tableName)}`);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/table/${encodeURIComponent(tableName)}/add" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Страница редактирования записи
app.get('/table/:tableName/edit/:id', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const id = req.params.id;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const record = db.getRecord(tableName, Number(id));
if (!record) {
throw new Error(`Запись с id=${id} не найдена.`);
}
const schema = db.getTableSchema(tableName);
const autoIncrement = db.getTableAutoIncrement(tableName);
let formFields = '';
for (let field in schema) {
if (field === 'id' && autoIncrement) {
// Если автоинкремент, не редактируем поле id
continue;
}
formFields += `
<div class="form-group animate__animated animate__fadeIn">
<label for="${field}">${field} (${schema[field]})</label>
${schema[field] === 'number' ? `
<input type="number" class="form-control" id="${field}" name="${field}" value="${record[field]}" required>
` : `
<input type="text" class="form-control" id="${field}" name="${field}" value="${record[field]}" required>
`}
</div>
`;
}
const content = `
<h1 class="animate__animated animate__fadeInDown">Редактировать Запись в Таблице: ${tableName}</h1>
<form action="/table/${encodeURIComponent(tableName)}/edit/${encodeURIComponent(id)}" method="POST" class="animate__animated animate__fadeIn">
${formFields}
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Сохранить Изменения</button>
<a href="/table/${encodeURIComponent(tableName)}" class="btn btn-secondary animate__animated animate__fadeIn">Отмена</a>
</form>
`;
renderPage(res, `Редактировать Запись в ${tableName}`, content);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Обработка редактирования записи
app.post('/table/:tableName/edit/:id', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const id = req.params.id;
const newRecord = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const schema = db.getTableSchema(tableName);
const autoIncrement = db.getTableAutoIncrement(tableName);
// Если автоинкремент для 'id', удаляем ручной ввод 'id' (если он был)
if (autoIncrement && newRecord.id !== undefined) {
delete newRecord.id;
}
// Преобразование типов полей
for (const field in newRecord) {
if (schema[field] === 'number') {
newRecord[field] = Number(newRecord[field]);
if (isNaN(newRecord[field])) {
throw new Error(`Поле "${field}" должно быть числом.`);
}
}
}
const success = db.updateRecord(tableName, Number(id), newRecord);
if (!success) {
throw new Error(`Запись с id=${id} не найдена.`);
}
res.redirect(`/table/${encodeURIComponent(tableName)}`);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/table/${encodeURIComponent(tableName)}/edit/${encodeURIComponent(id)}" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Обработка удаления записи
app.post('/table/:tableName/delete/:id', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const id = req.params.id;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const success = db.deleteRecord(tableName, Number(id));
if (!success) {
throw new Error(`Запись с id=${id} не найдена.`);
}
res.redirect(`/table/${encodeURIComponent(tableName)}`);
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/table/${encodeURIComponent(tableName)}" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// Обработка удаления таблицы
app.post('/drop-table/:tableName', isAuthenticated, (req, res) => {
const username = req.session.username;
const tableName = req.params.tableName;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
db.dropTable(tableName);
res.redirect('/');
} catch (error) {
const content = `
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
<hr>
<a href="/" class="btn btn-primary animate__animated animate__fadeIn">Назад</a>
</div>
`;
renderPage(res, 'Ошибка', content);
}
});
// -----------------------------------------------------------------------------------
// Новая функция: Вкладка "Выполнить Команду" с аккордеоном примеров
// -----------------------------------------------------------------------------------
// Страница выполнения команды
app.get('/execute-command', isAuthenticated, (req, res) => {
const username = req.session.username;
const userDBPath = path.join(BD_DIR, username, 'database.json');
const exampleQueries = `
<div class="accordion" id="exampleAccordion">
<div class="card">
<div class="card-header" id="headingOne">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Пример 1: Простый SELECT
</button>
</h2>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#exampleAccordion">
<div class="card-body">
<code>SELECT * FROM users;</code>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
Пример 2: SELECT с WHERE и ORDER BY
</button>
</h2>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#exampleAccordion">
<div class="card-body">
<code>SELECT id, name FROM users WHERE age >= 18 ORDER BY name ASC;</code>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingThree">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
Пример 3: SELECT с JOIN
</button>
</h2>
</div>
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#exampleAccordion">
<div class="card-body">
<code>
SELECT users.name, orders.id FROM users JOIN orders ON users.id = orders.user_id WHERE users.age > 25;
</code>
</div>
</div>
</div>
<!-- Добавьте дополнительные примеры по необходимости -->
</div>
`;
const user = findUser(username); // Получаем информацию о пользователе
const content = `
<h1 class="animate__animated animate__fadeInDown">Выполнить Команду</h1>
<div class="mb-4">
<h3>Ваш API Токен</h3>
<p>Используйте этот токен для API-запросов:</p>
<div class="input-group mb-3">
<input type="text" class="form-control" value="${user.token}" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" onclick="copyToken()">Копировать</button>
</div>
</div>
<button class="btn btn-warning" onclick="regenerateToken()">Сгенерировать новый токен</button>
</div>
<form action="/execute-command" method="POST" class="animate__animated animate__fadeIn">
<div class="form-group">
<label for="command">Введите вашу SQL-подобную команду:</label>
<textarea class="form-control" id="command" name="command" rows="4" placeholder="Например: SELECT * FROM users ORDER BY name ASC;" required></textarea>
</div>
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Выполнить</button>
<a href="/" class="btn btn-secondary animate__animated animate__fadeIn">Отмена</a>
</form>
<hr>
<h3>Примеры запросов:</h3>
${exampleQueries}
<script>
function copyToken() {
const tokenInput = document.querySelector('input[readonly]');
tokenInput.select();
tokenInput.setSelectionRange(0, 99999); // Для мобильных устройств
document.execCommand('copy');
alert('Токен скопирован в буфер обмена.');
}
function regenerateToken() {
if (confirm('Вы уверены, что хотите сгенерировать новый токен? Старый токен станет недействительным.')) {
fetch('/regenerate-token', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Новый токен сгенерирован.');
location.reload();
} else {
alert('Ошибка: ' + data.message);
}
})
.catch(err => {
alert('Произошла ошибка.');
});
}
}
</script>
`;
renderPage(res, 'Выполнить Команду', content);
});
// Обработка выполнения команды
app.post('/execute-command', isAuthenticated, (req, res) => {
const username = req.session.username;
const { command } = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const result = db.executeSQLCommand(command);
// Форматирование результата для отображения
let displayResult;
if (Array.isArray(result)) {
displayResult = result;
} else if (result.updated !== undefined) {
displayResult = { updated: result.updated };
} else if (result.deleted !== undefined) {
displayResult = { deleted: result.deleted };
} else {
displayResult = result;
}
const content = `
<h1 class="animate__animated animate__fadeInDown">Выполнить Команду</h1>
<form action="/execute-command" method="POST" class="animate__animated animate__fadeIn">
<div class="form-group">
<label for="command">Введите вашу SQL-подобную команду:</label>
<textarea class="form-control" id="command" name="command" rows="4" required>${escapeHTML(command)}</textarea>
</div>
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Выполнить</button>
<a href="/" class="btn btn-secondary animate__animated animate__fadeIn">Отмена</a>
</form>
<hr class="animate__animated animate__fadeIn">
<h3 class="animate__animated animate__fadeInUp">Результат:</h3>
<pre id="command-result" class="animate__animated animate__fadeInUp">${JSON.stringify(displayResult, null, 2)}</pre>
<a href="/execute-command" class="btn btn-secondary mt-3 animate__animated animate__fadeIn">Выполнить другую команду</a>
`;
renderPage(res, 'Выполнить Команду', content);
} catch (error) {
const content = `
<h1 class="animate__animated animate__fadeInDown">Выполнить Команду</h1>
<form action="/execute-command" method="POST" class="animate__animated animate__fadeIn">
<div class="form-group">
<label for="command">Введите вашу SQL-подобную команду:</label>
<textarea class="form-control" id="command" name="command" rows="4" required>${escapeHTML(command)}</textarea>
</div>
<button type="submit" class="btn btn-primary animate__animated animate__fadeIn">Выполнить</button>
<a href="/" class="btn btn-secondary animate__animated animate__fadeIn">Отмена</a>
</form>
<hr class="animate__animated animate__fadeIn">
<div class="alert alert-danger animate__animated animate__fadeIn" role="alert">
<h4 class="alert-heading">Ошибка</h4>
<p>${escapeHTML(error.message)}</p>
</div>
<a href="/execute-command" class="btn btn-secondary animate__animated animate__fadeIn">Попробовать снова</a>
`;
renderPage(res, 'Ошибка', content);
}
});
// -----------------------------------------------------------------------------------
// API-интерфейс с токен-аутентификацией
// -----------------------------------------------------------------------------------
// Маршруты API начинаются с /api
// Все API маршруты требуют передать токен в заголовке Authorization: Bearer <token>
// Middleware для проверки токена
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader) return res.status(401).json({ error: 'Нет заголовка авторизации.' });
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') {
return res.status(401).json({ error: 'Неверный формат заголовка авторизации.' });
}
const token = tokenParts[1];
const user = findUserByToken(token);
if (!user) {
return res.status(403).json({ error: 'Неверный токен.' });
}
// При успешной аутентификации добавляем пользователя в объект запроса
req.user = user;
next();
}
// Пример API маршрута: Получение списка таблиц
app.get('/api/tables', authenticateToken, (req, res) => {
const username = req.user.username;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const tables = db.getTables();
res.json({ tables });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Пример API маршрута: Создание таблицы
app.post('/api/create-table', authenticateToken, (req, res) => {
const username = req.user.username;
const { tableName, schema, autoIncrement } = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
if (!tableName || !schema || typeof schema !== 'object') {
throw new Error('Необходимо указать tableName и схемы таблицы.');
}
const db = new Database(userDBPath);
db.loadFromFile();
db.createTable(tableName, schema, autoIncrement);
res.json({ success: true, message: `Таблица "${tableName}" создана.` });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Пример API маршрута: Вставка записи
app.post('/api/insert', authenticateToken, (req, res) => {
const username = req.user.username;
const { tableName, record } = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
if (!tableName || !record || typeof record !== 'object') {
throw new Error('Необходимо указать tableName и запись.');
}
const db = new Database(userDBPath);
db.loadFromFile();
db.insert(tableName, record);
res.json({ success: true, message: 'Запись вставлена успешно.' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Пример API маршрута: Получение записи по id
app.get('/api/record/:tableName/:id', authenticateToken, (req, res) => {
const username = req.user.username;
const { tableName, id } = req.params;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const record = db.getRecord(tableName, Number(id));
if (!record) {
throw new Error(`Запись с id=${id} не найдена.`);
}
res.json({ record });
} catch (error) {
res.status(404).json({ error: error.message });
}
});
// Пример API маршрута: Обновление записи по id
app.put('/api/update/:tableName/:id', authenticateToken, (req, res) => {
const username = req.user.username;
const { tableName, id } = req.params;
const newRecord = req.body;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
if (!newRecord || typeof newRecord !== 'object') {
throw new Error('Необходимо предоставить новые данные для записи.');
}
const db = new Database(userDBPath);
db.loadFromFile();
const success = db.updateRecord(tableName, Number(id), newRecord);
if (!success) {
throw new Error(`Запись с id=${id} не найдена.`);
}
res.json({ success: true, message: `Запись с id=${id} обновлена.` });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Пример API маршрута: Удаление записи по id
app.delete('/api/delete/:tableName/:id', authenticateToken, (req, res) => {
const username = req.user.username;
const { tableName, id } = req.params;
const userDBPath = path.join(BD_DIR, username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
const success = db.deleteRecord(tableName, Number(id));
if (!success) {
throw new Error(`Запись с id=${id} не найдена.`);
}
res.json({ success: true, message: `Запись с id=${id} удалена.` });
} catch (error) {
res.status(404).json({ error: error.message });
}
});
// -----------------------------------------------------------------------------------
// Завершение работы и сохранение данных
// -----------------------------------------------------------------------------------
// Запуск сервера
const server = app.listen(PORT, () => {
console.log(`Сервер запущен на http://localhost:${PORT}`);
});
// Обработка завершения работы для дополнительного сохранения данных (опционально)
process.on('SIGINT', () => {
console.log('\nЗавершение работы...');
saveUsers();
server.close(() => {
console.log('Сервер остановлен.');
process.exit(0);
});
});
// -----------------------------------------------------------------------------------
// Вспомогательная функция для экранирования HTML
// -----------------------------------------------------------------------------------
function escapeHTML(str) {
if (typeof str !== 'string') return str;
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}ASSISTANT
Разделение вашего монолитного файла `app.js` на отдельные компоненты для бэкенда и фронтенда поможет сделать проект более организованным, поддерживаемым и масштабируемым. Ниже приведен пример структуры проекта, а также рекомендации по разделению кода на соответствующие части.
## Структура Проекта
Вот предложенная структура папок и файлов для вашего приложения:
```
project-root/
├── backend/
│ ├── controllers/
│ │ ├── authController.js
│ │ ├── tableController.js
│ │ └── commandController.js
│ ├── middleware/
│ │ └── authMiddleware.js
│ ├── models/
│ │ ├── Database.js
│ │ └── Table.js
│ ├── routes/
│ │ ├── authRoutes.js
│ │ ├── tableRoutes.js
│ │ └── apiRoutes.js
│ ├── utils/
│ │ └── escapeHTML.js
│ ├── app.js
│ └── package.json
├── frontend/
│ ├── views/
│ │ ├── layout.ejs
│ │ ├── login.ejs
│ │ ├── register.ejs
│ │ ├── tables.ejs
│ │ ├── createTable.ejs
│ │ ├── manageSchema.ejs
│ │ ├── table.ejs
│ │ ├── addRecord.ejs
│ │ ├── editRecord.ejs
│ │ └── executeCommand.ejs
│ ├── public/
│ │ ├── css/
│ │ │ └── styles.css
│ │ ├── js/
│ │ │ └── scripts.js
│ │ └── images/
│ └── package.json
├── BD/
│ └── users.json
└── README.md
```
### Описание Структуры
- **backend/**: Содержит все серверные компоненты приложения.
- **controllers/**: Логика обработки запросов.
- `authController.js`: Регистрация, вход, выход пользователей.
- `tableController.js`: Управление таблицами, записями.
- `commandController.js`: Выполнение SQL-подобных команд.
- **middleware/**: Промежуточные обработчики.
- `authMiddleware.js`: Проверка аутентификации.
- **models/**: Модели данных.
- `Database.js`: Класс Database.
- `Table.js`: Класс Table.
- **routes/**: Определение маршрутов.
- `authRoutes.js`: Маршруты аутентификации.
- `tableRoutes.js`: Маршруты управления таблицами.
- `apiRoutes.js`: API маршруты.
- **utils/**: Вспомогательные функции.
- `escapeHTML.js`: Функция для экранирования HTML.
- `app.js`: Главный файл приложения.
- `package.json`: Зависимости и скрипты бэкенда.
- **frontend/**: Содержит все фронтенд-компоненты приложения.
- **views/**: Шаблоны для рендеринга страниц (используется EJS).
- `layout.ejs`: Общий шаблон для всех страниц.
- Остальные `.ejs` файлы соответствуют разным страницам приложения.
- **public/**: Статические файлы.
- **css/**: Стили.
- **js/**: Клиентские скрипты.
- **images/**: Изображения.
- `package.json`: Зависимости и скрипты фронтенда (при необходимости).
- **BD/**: Директория базы данных.
- `users.json`: Файл с пользователями и их данными.
- `README.md`: Документация проекта.
## Шаги по Разделению Кода
### 1. Настройка Бэкенда
#### a. Инициализация Проекта Бэкенда
Перейдите в папку `backend/` и инициализируйте новый Node.js проект:
```bash
cd project-root/backend
npm init -y
```
Установите необходимые зависимости:
```bash
npm install express body-parser gpu.js express-session bcrypt uuid ejs
```
#### b. Создание Моделей
Создайте файлы `Database.js` и `Table.js` в папке `models/` и переместите соответствующие классы из `app.js`:
```javascript
// backend/models/Table.js
const { GPU } = require('gpu.js');
class Table {
constructor(name, schema, autoIncrement = false) {
this.name = name;
this.schema = schema;
this.autoIncrement = autoIncrement;
this.autoIncrementValue = 1;
this.gpu = new GPU();
this.columns = {};
for (let field in schema) {
this.columns[field] = schema[field] === 'number' ? [] : [];
}
}
// ... Остальные методы (insert, deleteRecord, updateRecord, getRecord, etc.)
}
module.exports = Table;
```
```javascript
// backend/models/Database.js
const fs = require('fs');
const path = require('path');
const Table = require('./Table');
const { parseSQL, evaluateWhere } = require('../utils/sqlParser'); // Перенесите функции парсинга SQL
class Database {
constructor(filePath) {
this.tables = new Map();
this.filePath = filePath;
}
// ... Методы createTable, dropTable, insert, deleteRecord, updateRecord, getRecord, getAll, executeSQLCommand, saveToFile, loadFromFile
}
module.exports = Database;
```
#### c. Создание Контроллеров
Создайте контроллеры для аутентификации, управления таблицами и выполнения команд.
```javascript
// backend/controllers/authController.js
const bcrypt = require('bcrypt');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const path = require('path');
const escapeHTML = require('../utils/escapeHTML');
const BD_DIR = path.join(__dirname, '../BD');
const USERS_FILE = path.join(BD_DIR, 'users.json');
const SALT_ROUNDS = 10;
// Убедитесь, что папка BD и файл users.json существуют
if (!fs.existsSync(BD_DIR)) fs.mkdirSync(BD_DIR);
if (!fs.existsSync(USERS_FILE)) fs.writeFileSync(USERS_FILE, JSON.stringify([]), 'utf8');
let users = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
function saveUsers() {
fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2), 'utf8');
}
function findUser(username) {
return users.find(user => user.username === username);
}
function findUserByToken(token) {
return users.find(user => user.token === token);
}
// Регистрация пользователя
async function register(req, res) {
const { username, password } = req.body;
try {
if (findUser(username)) throw new Error('Имя пользователя уже занято.');
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
const token = uuidv4();
const newUser = { username, passwordHash, token };
users.push(newUser);
saveUsers();
const userDir = path.join(BD_DIR, username);
if (!fs.existsSync(userDir)) fs.mkdirSync(userDir);
const userDBPath = path.join(userDir, 'database.json');
if (!fs.existsSync(userDBPath)) fs.writeFileSync(userDBPath, JSON.stringify({ tables: {} }, null, 2), 'utf8');
res.redirect('/login');
} catch (error) {
res.status(400).render('register', { error: escapeHTML(error.message) });
}
}
// Вход пользователя
async function login(req, res) {
const { username, password } = req.body;
try {
const user = findUser(username);
if (!user) throw new Error('Неверное имя пользователя или пароль.');
const match = await bcrypt.compare(password, user.passwordHash);
if (!match) throw new Error('Неверное имя пользователя или пароль.');
req.session.username = username;
res.redirect('/');
} catch (error) {
res.status(400).render('login', { error: escapeHTML(error.message) });
}
}
// Выход пользователя
function logout(req, res) {
req.session.destroy(() => {
res.redirect('/login');
});
}
module.exports = {
register,
login,
logout,
findUser,
findUserByToken,
saveUsers
};
```
```javascript
// backend/controllers/tableController.js
const path = require('path');
const Database = require('../models/Database');
const { escapeHTML } = require('../utils/escapeHTML');
function listTables(req, res) {
const username = req.session.username;
const userDBPath = path.join(__dirname, '../BD', username, 'database.json');
const db = new Database(userDBPath);
db.loadFromFile();
const tables = db.getTables();
res.render('tables', { tables, user: username });
}
function createTable(req, res) {
const username = req.session.username;
const userDBPath = path.join(__dirname, '../BD', username, 'database.json');
const { tableName, columnNames, columnTypes, autoIncrement } = req.body;
const db = new Database(userDBPath);
db.loadFromFile();
// Преобразование в массивы
const names = Array.isArray(columnNames) ? columnNames : [columnNames];
const types = Array.isArray(columnTypes) ? columnTypes : [columnTypes];
try {
const schema = {};
for (let i = 0; i < names.length; i++) {
const name = names[i].trim();
const type = types[i].trim().toLowerCase();
if (!name) throw new Error(`Имя колонки в строке ${i + 1} не может быть пустым.`);
if (!['number', 'string'].includes(type)) throw new Error(`Некорректный тип колонки "${name}" в строке ${i + 1}.`);
if (schema[name]) throw new Error(`Дублирующееся имя колонки: "${name}" в строке ${i + 1}.`);
schema[name] = type;
}
if (!schema.id) throw new Error('Необходимо указать колонку "id: number".');
if (schema.id !== 'number') throw new Error('Колонка "id" должна быть типа "number".');
const isAutoIncrement = autoIncrement === 'on';
db.createTable(tableName, schema, isAutoIncrement);
res.redirect('/');
} catch (error) {
res.status(400).render('createTable', { error: escapeHTML(error.message) });
}
}
// ... Другие контроллеры для управления схемой, записями и т.д.
module.exports = {
listTables,
createTable,
// Другие экспорты
};
```
#### d. Создание Маршрутов
Создайте файлы маршрутов и подключите контроллеры:
```javascript
// backend/routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
router.get('/register', (req, res) => {
res.render('register', { error: null });
});
router.post('/register', authController.register);
router.get('/login', (req, res) => {
res.render('login', { error: null });
});
router.post('/login', authController.login);
router.get('/logout', authController.logout);
module.exports = router;
```
```javascript
// backend/routes/tableRoutes.js
const express = require('express');
const router = express.Router();
const tableController = require('../controllers/tableController');
const { isAuthenticated } = require('../middleware/authMiddleware');
// Главная страница - список таблиц
router.get('/', isAuthenticated, tableController.listTables);
// Создание новой таблицы
router.get('/create-table', isAuthenticated, (req, res) => {
res.render('createTable', { error: null });
});
router.post('/create-table', isAuthenticated, tableController.createTable);
// ... Другие маршруты для управления схемой, записями и т.д.
module.exports = router;
```
```javascript
// backend/routes/apiRoutes.js
const express = require('express');
const router = express.Router();
const apiController = require('../controllers/apiController');
const { authenticateToken } = require('../middleware/authMiddleware');
// Примеры API маршрутов
router.get('/tables', authenticateToken, apiController.getTables);
router.post('/create-table', authenticateToken, apiController.createTable);
router.post('/insert', authenticateToken, apiController.insertRecord);
// ... Другие API маршруты
module.exports = router;
```
#### e. Создание Middleware
```javascript
// backend/middleware/authMiddleware.js
const { findUser } = require('../controllers/authController');
// Проверка пользовательской сессии
function isAuthenticated(req, res, next) {
if (req.session.username) {
res.locals.currentUser = req.session.username;
return next();
}
res.redirect('/login');
}
// Проверка API токена
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader) return res.status(401).json({ error: 'Нет заголовка авторизации.' });
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') {
return res.status(401).json({ error: 'Неверный формат заголовка авторизации.' });
}
const token = tokenParts[1];
const user = findUserByToken(token);
if (!user) {
return res.status(403).json({ error: 'Неверный токен.' });
}
req.user = user;
next();
}
module.exports = {
isAuthenticated,
authenticateToken
};
```
#### f. Основной Файл Приложения
```javascript
// backend/app.js
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const path = require('path');
// Маршруты
const authRoutes = require('./routes/authRoutes');
const tableRoutes = require('./routes/tableRoutes');
const apiRoutes = require('./routes/apiRoutes');
const app = express();
// Настройка EJS в качестве шаблонизатора
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../frontend/views'));
// Настройка статических файлов
app.use(express.static(path.join(__dirname, '../frontend/public')));
// Настройка промежуточного ПО
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(session({
secret: 'your-secret-key', // Замените на ваш секрет
resave: false,
saveUninitialized: false
}));
// Подключение маршрутов
app.use('/', authRoutes);
app.use('/', tableRoutes);
app.use('/api', apiRoutes);
// Главная страница (может быть перенаправлением из tableRoutes)
app.get('/', (req, res) => {
res.redirect('/tables');
});
// Обработка ошибок 404
app.use((req, res, next) => {
res.status(404).render('404');
});
// Запуск сервера
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Сервер запущен на http://localhost:${PORT}`);
});
```
### 2. Настройка Фронтенда
#### a. Использование Шаблонизатора EJS
Перейдите в папку `frontend/views/` и создайте файлы `.ejs` для различных страниц. Например:
```html
<!-- frontend/views/layout.ejs -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<!-- Animate.css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="/">Custom DB Manager</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<% if (currentUser) { %>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item <%= title === 'Список Таблиц' ? 'active' : '' %>">
<a class="nav-link" href="/">Таблицы</a>
</li>
<li class="nav-item <%= title === 'Создать Таблицу' ? 'active' : '' %>">
<a class="nav-link" href="/create-table">Создать Таблицу</a>
</li>
<li class="nav-item <%= title === 'Выполнить Команду' ? 'active' : '' %>">
<a class="nav-link" href="/execute-command">Выполнить Команду</a>
</li>
</ul>
<span class="navbar-text mr-3">
Привет, <%= currentUser %>!
</span>
<a href="/logout" class="btn btn-outline-light">Выйти</a>
</div>
<% } %>
</nav>
<div class="container mt-4">
<%- body %>
</div>
<!-- Bootstrap JS и зависимости -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<!-- Клиентские скрипты -->
<script src="/js/scripts.js"></script>
</body>
</html>
```
Использование общего макета `layout.ejs` позволяет избежать дублирования кода и обеспечить единообразие страниц.
#### b. Создание Страниц
Пример создания страницы регистрации:
```html
<!-- frontend/views/register.ejs -->
<% layout('layout', { title: 'Регистрация' }) %>
<h1>Регистрация</h1>
<% if (error) { %>
<div class="alert alert-danger"><%= error %></div>
<% } %>
<form action="/register" method="POST">
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Введите имя пользователя" required>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Введите пароль" required>
</div>
<button type="submit" class="btn btn-primary">Зарегистрироваться</button>
<a href="/login" class="btn btn-secondary">Войти</a>
</form>
```
Аналогично создайте другие страницы (`login.ejs`, `tables.ejs`, `createTable.ejs`, и т.д.) с использованием шаблона `layout.ejs`.
#### c. Статические Файлы
Перенесите все стили и скрипты в папку `frontend/public/`. Например, `styles.css` и `scripts.js`.
```css
/* frontend/public/css/styles.css */
body {
padding-top: 70px;
background-color: #f0f2f5;
font-family: 'Roboto', sans-serif;
}
/* Добавьте остальные стили по необходимости */
```
```javascript
// frontend/public/js/scripts.js
document.addEventListener('DOMContentLoaded', () => {
// Пример анимаций или других функций
});
```
### 3. Вспомогательные Функции и Утилиты
Перенесите функции `escapeHTML` и парсинг SQL в отдельные файлы.
```javascript
// backend/utils/escapeHTML.js
function escapeHTML(str) {
if (typeof str !== 'string') return str;
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
module.exports = escapeHTML;
```
```javascript
// backend/utils/sqlParser.js
// Перенесите функции parseSQL и parseWhere сюда
function parseSQL(command) {
// ... код функции parseSQL из app.js
}
function parseWhere(whereClause) {
// ... код функции parseWhere из app.js
}
function evaluateWhere(record, where) {
// ... код функции evaluateWhere из app.js
}
module.exports = {
parseSQL,
parseWhere,
evaluateWhere
};
```
### 4. Управление Пользователями
Логика управления пользователями уже была рассмотрена в `authController.js`. Убедитесь, что все вызовы функций сохранения и поиска пользователей соответствуют новым структурам.
### 5. API Маршруты
В `routes/apiRoutes.js` определите API маршруты, используя контроллеры. Например:
```javascript
// backend/controllers/apiController.js
const Database = require('../models/Database');
const { escapeHTML } = require('../utils/escapeHTML');
function getTables(req, res) {
const username = req.user.username;
const userDBPath = path.join(__dirname, '../BD', username, 'database.json');
try {
const db = new Database(userDBPath);
db.loadFromFile();
res.json({ tables: db.getTables() });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
function createTable(req, res) {
const username = req.user.username;
const { tableName, schema, autoIncrement } = req.body;
const userDBPath = path.join(__dirname, '../BD', username, 'database.json');
try {
if (!tableName || !schema || typeof schema !== 'object') {
throw new Error('Необходимо указать tableName и схемы таблицы.');
}
const db = new Database(userDBPath);
db.loadFromFile();
db.createTable(tableName, schema, autoIncrement);
res.json({ success: true, message: `Таблица "${tableName}" создана.` });
} catch (error) {
res.status(400).json({ error: error.message });
}
}
// ... Другие функции API
module.exports = {
getTables,
createTable,
// ... Другие экспорты
};
```
### 6. Завершение Настройки
#### a. Обновление `package.json`
Убедитесь, что в корневом `package.json` (если есть) или в `backend/package.json` указаны все необходимые скрипты. Например:
```json
// backend/package.json
{
"name": "custom-db-manager-backend",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"bcrypt": "^5.0.1",
"body-parser": "^1.19.0",
"ejs": "^3.1.6",
"express": "^4.17.1",
"express-session": "^1.17.1",
"gpu.js": "^2.10.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.7"
}
}
```
Установите `nodemon` для разработки:
```bash
npm install --save-dev nodemon
```
#### b. Запуск Приложения
Перейдите в папку `backend/` и запустите приложение:
```bash
cd project-root/backend
npm run dev
```
Откройте браузер и перейдите на `http://localhost:3000`, чтобы убедиться, что приложение работает.
## Дополнительные Рекомендации
1. **Использование ORM или Базы Данных**: Рассмотрите возможность использования ORM (например, Sequelize) и полноценных СУБД (например, PostgreSQL, MongoDB) для более надежного и масштабируемого хранения данных.
2. **Разделение Фронтенда и Бэкенда**: Если планируется масштабирование, можно разделить фронтенд и бэкенд на отдельные проекты. Фронтенд может быть реализован с использованием современных технологий (React, Vue.js, Angular) и взаимодействовать с бэкендом через API.
3. **Безопасность**: Убедитесь, что все входные данные корректно валидируются и экранируются, чтобы предотвратить атаки типа XSS, SQL-инъекций и т.д. Рассмотрите использование CSRF-токенов для защиты форм.
4. **Логирование и Отладка**: Реализуйте логирование запросов и ошибок с помощью таких библиотек, как `morgan` и `winston`.
5. **Тестирование**: Напишите тесты для бэкенд-логики и API с использованием Jest, Mocha или других тестовых фреймворков.
6. **Документация**: Добавьте документацию для вашего API, используя инструменты вроде Swagger.
## Заключение
Разделение приложения на бэкенд и фронтенд не только улучшит организацию кода, но и облегчит его поддержку и развитие в будущем. Следуя предложенной структуре и рекомендациям, вы сможете создать более структурированное и масштабируемое приложение.