feature/worker #111

Merged
primakov merged 190 commits from feature/worker into master 2025-12-05 16:59:42 +03:00
29 changed files with 1498 additions and 1827 deletions
Showing only changes of commit 0d1dcf21c1 - Show all commits

View File

@@ -1,31 +1,97 @@
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
console.log('\n📡 Попытка подключения к MongoDB...');
console.log(` URI: ${mongoUri.replace(/\/\/:.*@/, '//***:***@')}`);
const connection = await mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
console.log('✅ MongoDB подключена успешно!');
console.log(` Хост: ${connection.connection.host}`);
console.log(` БД: ${connection.connection.name}\n`);
return connection;
} catch (error) {
console.error('\n❌ Ошибка подключения к MongoDB:');
console.error(` ${error.message}\n`);
console.warn('⚠️ Приложение продолжит работу с mock данными\n');
return null;
// Get MongoDB URL from environment variables
// MONGO_ADDR is a centralized env variable from server/utils/const.ts
const primaryUri = process.env.MONGO_ADDR || process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
const fallbackUri = process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
/**
* Check if error is related to authentication
*/
const isAuthError = (error) => {
if (!error) {
return false;
}
const authCodes = new Set([18, 13]);
if (error.code && authCodes.has(error.code)) {
return true;
}
const message = String(error.message || '').toLowerCase();
return message.includes('auth') || message.includes('authentication');
};
/**
* Try to connect to MongoDB with specific URI
*/
const connectWithUri = async (uri, label) => {
console.log(`\n📡 Попытка подключения к MongoDB (${label})...`);
if (process.env.DEV === 'true') {
console.log(` URI: ${uri}`);
}
const connection = await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
try {
await connection.connection.db.admin().command({ ping: 1 });
} catch (pingError) {
if (isAuthError(pingError)) {
await mongoose.connection.close().catch(() => {});
throw pingError;
}
console.error('⚠️ MongoDB ping error:', pingError.message);
}
console.log('✅ MongoDB подключена успешно!');
console.log(` Хост: ${connection.connection.host}`);
console.log(` БД: ${connection.connection.name}\n`);
if (process.env.DEV === 'true') {
console.log(` Пользователь: ${connection.connection.user || 'anonymous'}`);
}
return connection;
};
/**
* Connect to MongoDB with fallback strategy
*/
const connectDB = async () => {
const attempts = [];
if (fallbackUri) {
attempts.push({ uri: fallbackUri, label: 'AUTH' });
}
attempts.push({ uri: primaryUri, label: 'PRIMARY' });
let lastError = null;
for (const attempt of attempts) {
try {
console.log(`[MongoDB] Trying ${attempt.label} connection...`);
return await connectWithUri(attempt.uri, attempt.label);
} catch (error) {
lastError = error;
console.error(`\n❌ Ошибка подключения к MongoDB (${attempt.label}):`);
console.error(` ${error.message}\n`);
if (!isAuthError(error)) {
break;
}
}
}
if (lastError) {
console.warn('⚠️ Приложение продолжит работу с mock данными\n');
}
return null;
};
module.exports = connectDB;

View File

@@ -1,8 +1,8 @@
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const { runMigrations } = require('./scripts/run-migrations');
const fs = require('fs');
const path = require('path');
// Загрузить переменные окружения
dotenv.config();
@@ -15,7 +15,7 @@ if (process.env.DEV === 'true') {
console.log(' DEBUG MODE ENABLED - All logs are visible');
}
// Импортировать маршруты
// Импортировать маршруты - прямые пути без path.join и __dirname
const authRoutes = require('./routes/auth');
const companiesRoutes = require('./routes/companies');
const messagesRoutes = require('./routes/messages');
@@ -28,34 +28,15 @@ const buyProductsRoutes = require('./routes/buyProducts');
const requestsRoutes = require('./routes/requests');
const homeRoutes = require('./routes/home');
const connectDB = require('./config/db');
const app = express();
// Подключить MongoDB и запустить миграции при инициализации
// Подключить MongoDB при инициализации
let dbConnected = false;
let migrationsCompleted = false;
const initializeApp = async () => {
try {
await connectDB().then(() => {
dbConnected = true;
});
// Запустить миграции после успешного подключения
if (dbConnected) {
try {
await runMigrations(false);
migrationsCompleted = true;
} catch (migrationError) {
console.error('⚠️ Migrations failed but app will continue:', migrationError.message);
}
}
} catch (err) {
console.error('Error during app initialization:', err);
}
};
// Запустить инициализацию
initializeApp();
connectDB().then(() => {
dbConnected = true;
});
// Middleware
app.use(cors());
@@ -84,13 +65,19 @@ app.use((req, res, next) => {
const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms);
app.use(delay());
// Статика для загруженных файлов
const uploadsRoot = path.join(__dirname, '..', '..', 'remote-assets', 'uploads');
if (!fs.existsSync(uploadsRoot)) {
fs.mkdirSync(uploadsRoot, { recursive: true });
}
app.use('/uploads', express.static(uploadsRoot));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
api: 'running',
database: dbConnected ? 'mongodb' : 'mock',
migrations: migrationsCompleted ? 'completed' : 'pending',
timestamp: new Date().toISOString()
});
});

View File

@@ -1,46 +0,0 @@
{
"mockAuthResponse": {
"user": {
"id": "user-123",
"email": "test@company.com",
"firstName": "Иван",
"lastName": "Петров",
"position": "Генеральный директор"
},
"company": {
"id": "company-123",
"name": "ООО \"Тестовая Компания\"",
"inn": "7707083893",
"ogrn": "1027700132195",
"fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"",
"shortName": "ООО \"Тест\"",
"legalForm": "ООО",
"industry": "Производство",
"companySize": "50-100",
"website": "https://test-company.ru",
"verified": true,
"rating": 4.5
},
"tokens": {
"accessToken": "mock-access-token-{{timestamp}}",
"refreshToken": "mock-refresh-token-{{timestamp}}"
}
},
"errorMessages": {
"validationFailed": "Заполните все обязательные поля",
"emailRequired": "Email обязателен",
"passwordRequired": "Пароль обязателен",
"termsRequired": "Необходимо принять условия использования",
"invalidCredentials": "Неверный email или пароль",
"refreshTokenRequired": "Refresh token обязателен",
"innValidation": "ИНН должен содержать 10 или 12 цифр"
},
"successMessages": {
"logoutSuccess": "Успешный выход",
"emailVerified": "Email успешно подтвержден",
"passwordResetSent": "Письмо для восстановления пароля отправлено",
"passwordResetSuccess": "Пароль успешно изменен",
"logoUploaded": "Логотип успешно загружен",
"addedToFavorites": "Добавлено в избранное"
}
}

View File

@@ -1,430 +0,0 @@
{
"mockCompany": {
"id": "company-123",
"name": "ООО \"Тестовая Компания\"",
"inn": "7707083893",
"ogrn": "1027700132195",
"fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"",
"shortName": "ООО \"Тест\"",
"legalForm": "ООО",
"industry": "Производство",
"companySize": "50-100",
"website": "https://test-company.ru",
"verified": true,
"rating": 4.5
},
"mockINNData": {
"7707083893": {
"name": "ПУБЛИЧНОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО \"СБЕРБАНК РОССИИ\"",
"ogrn": "1027700132195",
"legal_form": "ПАО"
},
"7730048036": {
"name": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ \"КОМПАНИЯ\"",
"ogrn": "1047730048036",
"legal_form": "ООО"
}
},
"mockCompanies": [
{
"id": "company-1",
"inn": "7707083893",
"ogrn": "1027700132195",
"fullName": "Общество с ограниченной ответственностью \"СтройКомплект\"",
"shortName": "ООО \"СтройКомплект\"",
"legalForm": "ООО",
"industry": "Строительство",
"companySize": "100-250",
"website": "https://stroykomplekt.ru",
"logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=SK",
"slogan": "Строим будущее вместе",
"rating": 4.8,
"verified": true,
"phone": "+7 (495) 123-45-67",
"email": "info@stroykomplekt.ru",
"legalAddress": "г. Москва, ул. Строительная, д. 15",
"foundedYear": 2010,
"employeeCount": "150 сотрудников"
},
{
"id": "company-6",
"inn": "7707083894",
"ogrn": "1027700132196",
"fullName": "Акционерное общество \"Московский Строй\"",
"shortName": "АО \"Московский Строй\"",
"legalForm": "АО",
"industry": "Строительство",
"companySize": "500+",
"website": "https://moscow-stroy.ru",
"logo": "https://via.placeholder.com/100x100/1A365D/FFFFFF?text=MS",
"slogan": "Качество и надежность с 1995 года",
"rating": 4.9,
"verified": true,
"phone": "+7 (495) 987-65-43",
"email": "info@moscow-stroy.ru",
"legalAddress": "г. Москва, пр. Мира, д. 100",
"foundedYear": 1995,
"employeeCount": "800+ сотрудников"
},
{
"id": "company-7",
"inn": "7707083895",
"ogrn": "1027700132197",
"fullName": "Общество с ограниченной ответственностью \"ДомСтрой\"",
"shortName": "ООО \"ДомСтрой\"",
"legalForm": "ООО",
"industry": "Строительство",
"companySize": "50-100",
"website": "https://domstroy.ru",
"logo": "https://via.placeholder.com/100x100/2D3748/FFFFFF?text=DS",
"slogan": "Строим дома мечты",
"rating": 4.3,
"verified": true,
"phone": "+7 (495) 555-12-34",
"email": "info@domstroy.ru",
"legalAddress": "г. Москва, ул. Жилстроительная, д. 25",
"foundedYear": 2015,
"employeeCount": "75 сотрудников"
},
{
"id": "company-4",
"inn": "7730048038",
"ogrn": "1047730048038",
"fullName": "Общество с ограниченной ответственностью \"МеталлПром\"",
"shortName": "ООО \"МеталлПром\"",
"legalForm": "ООО",
"industry": "Производство",
"companySize": "250-500",
"website": "https://metallprom.ru",
"logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MP",
"slogan": "Металл высшего качества",
"rating": 4.7,
"verified": true,
"phone": "+7 (495) 456-78-90",
"email": "info@metallprom.ru",
"legalAddress": "г. Москва, ул. Промышленная, д. 50",
"foundedYear": 2008,
"employeeCount": "300 сотрудников"
},
{
"id": "company-8",
"inn": "7730048040",
"ogrn": "1047730048040",
"fullName": "Общество с ограниченной ответственностью \"СтальМет\"",
"shortName": "ООО \"СтальМет\"",
"legalForm": "ООО",
"industry": "Производство",
"companySize": "100-250",
"website": "https://stalmet.ru",
"logo": "https://via.placeholder.com/100x100/9C4221/FFFFFF?text=SM",
"slogan": "Сталь для промышленности",
"rating": 4.6,
"verified": true,
"phone": "+7 (495) 777-88-99",
"email": "sales@stalmet.ru",
"legalAddress": "г. Москва, ул. Металлургическая, д. 30",
"foundedYear": 2012,
"employeeCount": "180 сотрудников"
},
{
"id": "company-9",
"inn": "7730048041",
"ogrn": "1047730048041",
"fullName": "Общество с ограниченной ответственностью \"ПластМаш\"",
"shortName": "ООО \"ПластМаш\"",
"legalForm": "ООО",
"industry": "Производство",
"companySize": "50-100",
"website": "https://plastmash.ru",
"logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PM",
"slogan": "Пластиковые изделия для всех отраслей",
"rating": 4.4,
"verified": true,
"phone": "+7 (495) 333-44-55",
"email": "info@plastmash.ru",
"legalAddress": "г. Москва, ул. Пластиковая, д. 12",
"foundedYear": 2018,
"employeeCount": "80 сотрудников"
},
{
"id": "company-2",
"inn": "7730048036",
"ogrn": "1047730048036",
"fullName": "Общество с ограниченной ответственностью \"ТехСнаб\"",
"shortName": "ООО \"ТехСнаб\"",
"legalForm": "ООО",
"industry": "Торговля",
"companySize": "50-100",
"website": "https://techsnab.ru",
"logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=TS",
"slogan": "Снабжение для профессионалов",
"rating": 4.5,
"verified": true,
"phone": "+7 (495) 234-56-78",
"email": "sales@techsnab.ru",
"legalAddress": "г. Москва, ул. Торговая, д. 8",
"foundedYear": 2010,
"employeeCount": "90 сотрудников"
},
{
"id": "company-10",
"inn": "7730048042",
"ogrn": "1047730048042",
"fullName": "Общество с ограниченной ответственностью \"ОптТорг\"",
"shortName": "ООО \"ОптТорг\"",
"legalForm": "ООО",
"industry": "Торговля",
"companySize": "100-250",
"website": "https://opttorg.ru",
"logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=OT",
"slogan": "Оптовые поставки по всей России",
"rating": 4.2,
"verified": true,
"phone": "+7 (495) 111-22-33",
"email": "info@opttorg.ru",
"legalAddress": "г. Москва, ул. Оптовая, д. 45",
"foundedYear": 2005,
"employeeCount": "200 сотрудников"
},
{
"id": "company-5",
"inn": "7730048039",
"ogrn": "1047730048039",
"fullName": "Общество с ограниченной ответственностью \"ЛогистикПлюс\"",
"shortName": "ООО \"ЛогистикПлюс\"",
"legalForm": "ООО",
"industry": "Логистика",
"companySize": "100-250",
"website": "https://logistikplus.ru",
"logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=LP",
"slogan": "Доставляем быстро и надежно",
"rating": 4.6,
"verified": true,
"phone": "+7 (495) 567-89-01",
"email": "info@logistikplus.ru",
"legalAddress": "г. Москва, ул. Логистическая, д. 20",
"foundedYear": 2013,
"employeeCount": "150 сотрудников"
},
{
"id": "company-11",
"inn": "7730048043",
"ogrn": "1047730048043",
"fullName": "Общество с ограниченной ответственностью \"ТрансЛогист\"",
"shortName": "ООО \"ТрансЛогист\"",
"legalForm": "ООО",
"industry": "Логистика",
"companySize": "250-500",
"website": "https://translogist.ru",
"logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=TL",
"slogan": "Транспортные решения для бизнеса",
"rating": 4.8,
"verified": true,
"phone": "+7 (495) 999-88-77",
"email": "info@translogist.ru",
"legalAddress": "г. Москва, ул. Транспортная, д. 60",
"foundedYear": 2007,
"employeeCount": "350 сотрудников"
},
{
"id": "company-12",
"inn": "7730048044",
"ogrn": "1047730048044",
"fullName": "Общество с ограниченной ответственностью \"ТехСофт\"",
"shortName": "ООО \"ТехСофт\"",
"legalForm": "ООО",
"industry": "IT",
"companySize": "50-100",
"website": "https://techsoft.ru",
"logo": "https://via.placeholder.com/100x100/3182CE/FFFFFF?text=TS",
"slogan": "IT-решения для бизнеса",
"rating": 4.7,
"verified": true,
"phone": "+7 (495) 444-55-66",
"email": "info@techsoft.ru",
"legalAddress": "г. Москва, ул. Программистов, д. 10",
"foundedYear": 2016,
"employeeCount": "85 сотрудников"
},
{
"id": "company-13",
"inn": "7730048045",
"ogrn": "1047730048045",
"fullName": "Общество с ограниченной ответственностью \"КиберТех\"",
"shortName": "ООО \"КиберТех\"",
"legalForm": "ООО",
"industry": "IT",
"companySize": "100-250",
"website": "https://cybertech.ru",
"logo": "https://via.placeholder.com/100x100/553C9A/FFFFFF?text=CT",
"slogan": "Кибербезопасность и автоматизация",
"rating": 4.9,
"verified": true,
"phone": "+7 (495) 666-77-88",
"email": "info@cybertech.ru",
"legalAddress": "г. Москва, ул. Кибернетическая, д. 5",
"foundedYear": 2014,
"employeeCount": "120 сотрудников"
},
{
"id": "company-3",
"inn": "7730048037",
"ogrn": "1047730048037",
"fullName": "Индивидуальный предприниматель Сидоров Петр Иванович",
"shortName": "ИП Сидоров П.И.",
"legalForm": "ИП",
"industry": "Услуги",
"companySize": "1-10",
"website": "https://sidorov-service.ru",
"logo": "https://via.placeholder.com/100x100/D69E2E/FFFFFF?text=SI",
"slogan": "Качественные услуги для малого бизнеса",
"rating": 4.2,
"verified": false,
"phone": "+7 (495) 345-67-89",
"email": "info@sidorov-service.ru",
"legalAddress": "г. Москва, ул. Сервисная, д. 3",
"foundedYear": 2020,
"employeeCount": "5 сотрудников"
},
{
"id": "company-14",
"inn": "7730048046",
"ogrn": "1047730048046",
"fullName": "Общество с ограниченной ответственностью \"КонсалтПро\"",
"shortName": "ООО \"КонсалтПро\"",
"legalForm": "ООО",
"industry": "Услуги",
"companySize": "10-50",
"website": "https://konsultpro.ru",
"logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=KP",
"slogan": "Консалтинг для роста бизнеса",
"rating": 4.5,
"verified": true,
"phone": "+7 (495) 222-33-44",
"email": "info@konsultpro.ru",
"legalAddress": "г. Москва, ул. Консультационная, д. 15",
"foundedYear": 2017,
"employeeCount": "25 сотрудников"
},
{
"id": "company-15",
"inn": "7730048047",
"ogrn": "1047730048047",
"fullName": "Общество с ограниченной ответственностью \"ПищеПром\"",
"shortName": "ООО \"ПищеПром\"",
"legalForm": "ООО",
"industry": "Пищевая промышленность",
"companySize": "100-250",
"website": "https://pishcheprom.ru",
"logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PP",
"slogan": "Качественные продукты питания",
"rating": 4.4,
"verified": true,
"phone": "+7 (495) 888-99-00",
"email": "info@pishcheprom.ru",
"legalAddress": "г. Москва, ул. Пищевая, д. 40",
"foundedYear": 2011,
"employeeCount": "180 сотрудников"
},
{
"id": "company-16",
"inn": "7730048048",
"ogrn": "1047730048048",
"fullName": "Общество с ограниченной ответственностью \"ЭнергоСервис\"",
"shortName": "ООО \"ЭнергоСервис\"",
"legalForm": "ООО",
"industry": "Энергетика",
"companySize": "50-100",
"website": "https://energoservice.ru",
"logo": "https://via.placeholder.com/100x100/F6AD55/FFFFFF?text=ES",
"slogan": "Энергетические решения",
"rating": 4.6,
"verified": true,
"phone": "+7 (495) 555-66-77",
"email": "info@energoservice.ru",
"legalAddress": "г. Москва, ул. Энергетическая, д. 25",
"foundedYear": 2013,
"employeeCount": "70 сотрудников"
},
{
"id": "company-17",
"inn": "7730048049",
"ogrn": "1047730048049",
"fullName": "Общество с ограниченной ответственностью \"МедТех\"",
"shortName": "ООО \"МедТех\"",
"legalForm": "ООО",
"industry": "Медицина",
"companySize": "100-250",
"website": "https://medtech.ru",
"logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MT",
"slogan": "Медицинские технологии будущего",
"rating": 4.8,
"verified": true,
"phone": "+7 (495) 777-00-11",
"email": "info@medtech.ru",
"legalAddress": "г. Москва, ул. Медицинская, д. 35",
"foundedYear": 2015,
"employeeCount": "200 сотрудников"
},
{
"id": "company-18",
"inn": "7730048050",
"ogrn": "1047730048050",
"fullName": "Общество с ограниченной ответственностью \"ОбразЦентр\"",
"shortName": "ООО \"ОбразЦентр\"",
"legalForm": "ООО",
"industry": "Образование",
"companySize": "50-100",
"website": "https://obrazcentr.ru",
"logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=OC",
"slogan": "Образование и развитие персонала",
"rating": 4.3,
"verified": true,
"phone": "+7 (495) 333-00-22",
"email": "info@obrazcentr.ru",
"legalAddress": "г. Москва, ул. Образовательная, д. 18",
"foundedYear": 2018,
"employeeCount": "60 сотрудников"
},
{
"id": "company-19",
"inn": "7730048051",
"ogrn": "1047730048051",
"fullName": "Общество с ограниченной ответственностью \"ФинКонсалт\"",
"shortName": "ООО \"ФинКонсалт\"",
"legalForm": "ООО",
"industry": "Финансы",
"companySize": "10-50",
"website": "https://finkonsalt.ru",
"logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=FK",
"slogan": "Финансовое консультирование",
"rating": 4.7,
"verified": true,
"phone": "+7 (495) 444-00-33",
"email": "info@finkonsalt.ru",
"legalAddress": "г. Москва, ул. Финансовая, д. 12",
"foundedYear": 2016,
"employeeCount": "35 сотрудников"
},
{
"id": "company-20",
"inn": "7730048052",
"ogrn": "1047730048052",
"fullName": "Общество с ограниченной ответственностью \"АгроТех\"",
"shortName": "ООО \"АгроТех\"",
"legalForm": "ООО",
"industry": "Сельское хозяйство",
"companySize": "100-250",
"website": "https://agrotech.ru",
"logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=AT",
"slogan": "Современные технологии в сельском хозяйстве",
"rating": 4.5,
"verified": true,
"phone": "+7 (495) 666-00-44",
"email": "info@agrotech.ru",
"legalAddress": "г. Москва, ул. Аграрная, д. 28",
"foundedYear": 2012,
"employeeCount": "160 сотрудников"
}
]
}

View File

@@ -1,158 +0,0 @@
{
"mockProducts": [
{
"id": "prod-1",
"name": "Металлические конструкции",
"description": "Производство и поставка металлических конструкций любой сложности для строительства",
"category": "Строительные материалы",
"type": "sell",
"companyId": "company-4",
"price": "от 50 000 руб/тонна",
"createdAt": "{{date-10-days}}",
"updatedAt": "{{date-2-days}}"
},
{
"id": "prod-2",
"name": "Стальные балки и профили",
"description": "Высококачественные стальные балки и профили для промышленного строительства",
"category": "Металлопрокат",
"type": "sell",
"companyId": "company-8",
"price": "от 45 000 руб/тонна",
"createdAt": "{{date-8-days}}",
"updatedAt": "{{date-1-day}}"
},
{
"id": "prod-3",
"name": "Пластиковые изделия",
"description": "Производство пластиковых изделий для различных отраслей промышленности",
"category": "Пластик",
"type": "sell",
"companyId": "company-9",
"price": "от 200 руб/кг",
"createdAt": "{{date-15-days}}",
"updatedAt": "{{date-3-days}}"
},
{
"id": "prod-4",
"name": "Строительные материалы",
"description": "Полный спектр строительных материалов для жилого и коммерческого строительства",
"category": "Строительные материалы",
"type": "sell",
"companyId": "company-1",
"price": "по запросу",
"createdAt": "{{date-20-days}}",
"updatedAt": "{{date-5-days}}"
},
{
"id": "prod-5",
"name": "IT-решения для бизнеса",
"description": "Разработка программного обеспечения и IT-консалтинг для предприятий",
"category": "IT-услуги",
"type": "sell",
"companyId": "company-12",
"price": "от 100 000 руб/проект",
"createdAt": "{{date-12-days}}",
"updatedAt": "{{date-2-days}}"
},
{
"id": "prod-6",
"name": "Логистические услуги",
"description": "Комплексные логистические услуги по всей России и СНГ",
"category": "Логистика",
"type": "sell",
"companyId": "company-5",
"price": "от 15 руб/км",
"createdAt": "{{date-18-days}}",
"updatedAt": "{{date-4-days}}"
},
{
"id": "prod-7",
"name": "Пищевая продукция",
"description": "Производство качественных продуктов питания для HoReCa и розничной торговли",
"category": "Пищевая продукция",
"type": "sell",
"companyId": "company-15",
"price": "по прайс-листу",
"createdAt": "{{date-25-days}}",
"updatedAt": "{{date-7-days}}"
},
{
"id": "prod-8",
"name": "Медицинское оборудование",
"description": "Поставка современного медицинского оборудования и расходных материалов",
"category": "Медицинское оборудование",
"type": "sell",
"companyId": "company-17",
"price": "по запросу",
"createdAt": "{{date-30-days}}",
"updatedAt": "{{date-10-days}}"
},
{
"id": "prod-9",
"name": "Запчасти для спецтехники",
"description": "Ищем надежного поставщика запчастей для строительной техники Caterpillar, Komatsu",
"category": "Запчасти",
"type": "buy",
"companyId": "company-2",
"budget": "до 500 000 руб",
"createdAt": "{{date-5-days}}",
"updatedAt": "{{date-1-day}}"
},
{
"id": "prod-10",
"name": "Сырье для производства",
"description": "Требуется качественное сырье для производства пластиковых изделий",
"category": "Сырье",
"type": "buy",
"companyId": "company-9",
"budget": "до 1 000 000 руб",
"createdAt": "{{date-7-days}}",
"updatedAt": "{{date-2-days}}"
},
{
"id": "prod-11",
"name": "IT-оборудование",
"description": "Закупка серверного оборудования и сетевого оборудования для офиса",
"category": "IT-оборудование",
"type": "buy",
"companyId": "company-13",
"budget": "до 2 000 000 руб",
"createdAt": "{{date-3-days}}",
"updatedAt": "{{date-1-day}}"
},
{
"id": "prod-12",
"name": "Консалтинговые услуги",
"description": "Требуется консультация по оптимизации бизнес-процессов",
"category": "Консалтинг",
"type": "buy",
"companyId": "company-14",
"budget": "до 300 000 руб",
"createdAt": "{{date-4-days}}",
"updatedAt": "{{date-1-day}}"
},
{
"id": "prod-13",
"name": "Образовательные программы",
"description": "Поиск поставщика корпоративного обучения для сотрудников",
"category": "Образование",
"type": "buy",
"companyId": "company-18",
"budget": "до 200 000 руб",
"createdAt": "{{date-6-days}}",
"updatedAt": "{{date-2-days}}"
},
{
"id": "prod-14",
"name": "Финансовые услуги",
"description": "Требуется консультация по инвестиционному планированию",
"category": "Финансовые услуги",
"type": "buy",
"companyId": "company-19",
"budget": "до 150 000 руб",
"createdAt": "{{date-2-days}}",
"updatedAt": "{{date-1-day}}"
}
]
}

View File

@@ -1,122 +0,0 @@
{
"suggestions": [
"Строительные материалы",
"Металлоконструкции",
"Логистические услуги",
"Промышленное оборудование",
"Запчасти для спецтехники",
"IT-решения",
"Консалтинговые услуги",
"Пищевая продукция",
"Энергетическое оборудование",
"Медицинские технологии",
"Образовательные услуги",
"Финансовые услуги",
"Сельскохозяйственная техника",
"Торговое оборудование",
"Производственные услуги"
],
"searchHistory": [
{
"query": "строительные материалы",
"timestamp": "{{date-1-day}}"
},
{
"query": "металлоконструкции",
"timestamp": "{{date-2-days}}"
},
{
"query": "логистические услуги",
"timestamp": "{{date-3-days}}"
},
{
"query": "IT-решения",
"timestamp": "{{date-5-days}}"
},
{
"query": "консалтинг",
"timestamp": "{{date-7-days}}"
},
{
"query": "пищевая продукция",
"timestamp": "{{date-10-days}}"
},
{
"query": "медицинское оборудование",
"timestamp": "{{date-12-days}}"
},
{
"query": "образовательные услуги",
"timestamp": "{{date-15-days}}"
},
{
"query": "финансовые услуги",
"timestamp": "{{date-18-days}}"
},
{
"query": "сельскохозяйственная техника",
"timestamp": "{{date-20-days}}"
}
],
"savedSearches": [
{
"id": "saved-1",
"name": "Строительные компании",
"params": {
"industries": ["Строительство"],
"minRating": 4.5
},
"createdAt": "{{date-7-days}}"
},
{
"id": "saved-2",
"name": "Поставщики металла",
"params": {
"query": "металл",
"industries": ["Производство"]
},
"createdAt": "{{date-14-days}}"
},
{
"id": "saved-3",
"name": "IT-компании",
"params": {
"industries": ["IT"],
"minRating": 4.0
},
"createdAt": "{{date-21-days}}"
},
{
"id": "saved-4",
"name": "Логистические услуги",
"params": {
"industries": ["Логистика"],
"companySize": ["100-250", "250-500"]
},
"createdAt": "{{date-28-days}}"
},
{
"id": "saved-5",
"name": "Консалтинговые услуги",
"params": {
"industries": ["Услуги"],
"minRating": 4.3
},
"createdAt": "{{date-35-days}}"
}
],
"recommendationReasons": {
"Строительство": "Отличная репутация в строительной сфере",
"Производство": "Высокое качество производимой продукции",
"Логистика": "Надежные логистические решения",
"Торговля": "Широкий ассортимент и быстрые поставки",
"IT": "Инновационные IT-решения",
"Услуги": "Профессиональные консалтинговые услуги",
"Пищевая промышленность": "Качественная пищевая продукция",
"Энергетика": "Энергоэффективные решения",
"Медицина": "Современные медицинские технологии",
"Образование": "Эффективные образовательные программы",
"Финансы": "Надежные финансовые услуги",
"Сельское хозяйство": "Современные агротехнологии"
}
}

View File

@@ -1,13 +0,0 @@
{
"mockUser": {
"id": "user-123",
"email": "test@company.com",
"firstName": "Иван",
"lastName": "Петров",
"position": "Генеральный директор"
},
"mockTokens": {
"accessToken": "mock-access-token-{{timestamp}}",
"refreshToken": "mock-refresh-token-{{timestamp}}"
}
}

View File

@@ -0,0 +1,43 @@
const mongoose = require('mongoose');
const buyDocumentSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true,
index: true
},
ownerCompanyId: {
type: String,
required: true,
index: true
},
name: {
type: String,
required: true
},
type: {
type: String,
required: true
},
size: {
type: Number,
required: true
},
filePath: {
type: String,
required: true
},
acceptedBy: {
type: [String],
default: []
},
createdAt: {
type: Date,
default: Date.now,
index: true
}
});
module.exports = mongoose.model('BuyDocument', buyDocumentSchema);

View File

@@ -30,6 +30,7 @@ const buyProductSchema = new mongoose.Schema({
url: String,
type: String,
size: Number,
storagePath: String,
uploadedAt: {
type: Date,
default: Date.now

View File

@@ -0,0 +1,46 @@
const mongoose = require('mongoose');
const experienceSchema = new mongoose.Schema({
companyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true,
index: true
},
confirmed: {
type: Boolean,
default: false
},
customer: {
type: String,
required: true
},
subject: {
type: String,
required: true
},
volume: {
type: String
},
contact: {
type: String
},
comment: {
type: String
},
createdAt: {
type: Date,
default: Date.now,
index: true
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Индексы для оптимизации поиска
experienceSchema.index({ companyId: 1, createdAt: -1 });
module.exports = mongoose.model('Experience', experienceSchema);

View File

@@ -11,6 +11,12 @@ const requestSchema = new mongoose.Schema({
required: true,
index: true
},
subject: {
type: String,
required: false,
trim: true,
default: ''
},
text: {
type: String,
required: true
@@ -21,6 +27,7 @@ const requestSchema = new mongoose.Schema({
url: String,
type: String,
size: Number,
storagePath: String,
uploadedAt: {
type: Date,
default: Date.now
@@ -39,6 +46,18 @@ const requestSchema = new mongoose.Schema({
type: String,
default: null
},
responseFiles: [{
id: String,
name: String,
url: String,
type: String,
size: Number,
storagePath: String,
uploadedAt: {
type: Date,
default: Date.now
}
}],
respondedAt: {
type: Date,
default: null
@@ -58,5 +77,6 @@ const requestSchema = new mongoose.Schema({
requestSchema.index({ senderCompanyId: 1, createdAt: -1 });
requestSchema.index({ recipientCompanyId: 1, createdAt: -1 });
requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 });
requestSchema.index({ subject: 1, createdAt: -1 });
module.exports = mongoose.model('Request', requestSchema);

View File

@@ -1,8 +1,101 @@
const express = require('express');
const router = express.Router();
const { generateToken } = require('../middleware/auth');
const { generateToken, verifyToken } = require('../middleware/auth');
const User = require('../models/User');
const Company = require('../models/Company');
const Request = require('../models/Request');
const BuyProduct = require('../models/BuyProduct');
const Message = require('../models/Message');
const Review = require('../models/Review');
const mongoose = require('mongoose');
const { Types } = mongoose;
const connectDB = require('../config/db');
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
const PRESET_USER_EMAIL = 'admin@test-company.ru';
const changePasswordFlow = async (userId, currentPassword, newPassword) => {
if (!currentPassword || !newPassword) {
return { status: 400, body: { error: 'Current password and new password are required' } };
}
if (typeof newPassword !== 'string' || newPassword.trim().length < 8) {
return { status: 400, body: { error: 'New password must be at least 8 characters long' } };
}
const user = await User.findById(userId);
if (!user) {
return { status: 404, body: { error: 'User not found' } };
}
const isMatch = await user.comparePassword(currentPassword);
if (!isMatch) {
return { status: 400, body: { error: 'Current password is incorrect' } };
}
user.password = newPassword;
user.updatedAt = new Date();
await user.save();
return { status: 200, body: { message: 'Password updated successfully' } };
};
const deleteAccountFlow = async (userId, password) => {
if (!password) {
return { status: 400, body: { error: 'Password is required to delete account' } };
}
const user = await User.findById(userId);
if (!user) {
return { status: 404, body: { error: 'User not found' } };
}
const validPassword = await user.comparePassword(password);
if (!validPassword) {
return { status: 400, body: { error: 'Password is incorrect' } };
}
const companyId = user.companyId ? user.companyId.toString() : null;
const companyObjectId = companyId && Types.ObjectId.isValid(companyId) ? new Types.ObjectId(companyId) : null;
const cleanupTasks = [];
if (companyId) {
cleanupTasks.push(Request.deleteMany({
$or: [{ senderCompanyId: companyId }, { recipientCompanyId: companyId }],
}));
cleanupTasks.push(BuyProduct.deleteMany({ companyId }));
if (companyObjectId) {
cleanupTasks.push(Message.deleteMany({
$or: [
{ senderCompanyId: companyObjectId },
{ recipientCompanyId: companyObjectId },
],
}));
cleanupTasks.push(Review.deleteMany({
$or: [
{ companyId: companyObjectId },
{ authorCompanyId: companyObjectId },
],
}));
}
cleanupTasks.push(Company.findByIdAndDelete(companyId));
}
cleanupTasks.push(User.findByIdAndDelete(user._id));
await Promise.all(cleanupTasks);
return { status: 200, body: { message: 'Account deleted successfully' } };
};
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
@@ -15,16 +108,65 @@ const log = (message, data = '') => {
}
};
// In-memory storage для логирования
let users = [];
const waitForDatabaseConnection = async () => {
const isAuthFailure = (error) => {
if (!error) return false;
if (error.code === 13 || error.code === 18) return true;
return /auth/i.test(String(error.message || ''));
};
const verifyAuth = async () => {
try {
await mongoose.connection.db.admin().command({ listDatabases: 1 });
return true;
} catch (error) {
if (isAuthFailure(error)) {
return false;
}
throw error;
}
};
for (let attempt = 0; attempt < 3; attempt++) {
if (mongoose.connection.readyState === 1) {
const authed = await verifyAuth();
if (authed) {
return;
}
await mongoose.connection.close().catch(() => {});
}
try {
const connection = await connectDB();
if (!connection) {
break;
}
const authed = await verifyAuth();
if (authed) {
return;
}
await mongoose.connection.close().catch(() => {});
} catch (error) {
if (!isAuthFailure(error)) {
throw error;
}
}
}
throw new Error('Unable to authenticate with MongoDB');
};
// Инициализация тестового пользователя
const initializeTestUser = async () => {
try {
const existingUser = await User.findOne({ email: 'admin@test-company.ru' });
if (!existingUser) {
// Создать компанию
const company = await Company.create({
await waitForDatabaseConnection();
let company = await Company.findById(PRESET_COMPANY_ID);
if (!company) {
company = await Company.create({
_id: PRESET_COMPANY_ID,
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
inn: '7707083893',
@@ -39,131 +181,61 @@ const initializeTestUser = async () => {
description: 'Ведущая компания в области производства',
slogan: 'Качество и инновация'
});
log('✅ Test company initialized');
} else {
await Company.updateOne(
{ _id: PRESET_COMPANY_ID },
{
$set: {
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
industry: 'Производство',
companySize: '50-100',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://test-company.ru',
},
}
);
}
// Создать пользователя
const user = await User.create({
email: 'admin@test-company.ru',
let existingUser = await User.findOne({ email: PRESET_USER_EMAIL });
if (!existingUser) {
existingUser = await User.create({
email: PRESET_USER_EMAIL,
password: 'SecurePass123!',
firstName: 'Иван',
lastName: 'Петров',
position: 'Генеральный директор',
companyId: company._id
companyId: PRESET_COMPANY_ID
});
log('✅ Test user initialized');
}
// Инициализация других тестовых компаний
const mockCompanies = [
{
fullName: 'ООО "СтройКомплект"',
shortName: 'ООО "СтройКомплект"',
inn: '7707083894',
ogrn: '1027700132196',
legalForm: 'ООО',
industry: 'Строительство',
companySize: '51-250',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://stroykomplekt.ru',
verified: true,
rating: 4.8,
description: 'Компания строит будущее вместе',
slogan: 'Строим будущее вместе'
},
{
fullName: 'АО "Московский Строй"',
shortName: 'АО "Московский Строй"',
inn: '7707083895',
ogrn: '1027700132197',
legalForm: 'АО',
industry: 'Строительство',
companySize: '500+',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://moscow-stroy.ru',
verified: true,
rating: 4.9,
description: 'Качество и надежность с 1995 года',
slogan: 'Качество и надежность'
},
{
fullName: 'ООО "Тероект"',
shortName: 'ООО "Тероект"',
inn: '7707083896',
ogrn: '1027700132198',
legalForm: 'ООО',
industry: 'IT',
companySize: '11-50',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://techproject.ru',
verified: true,
rating: 4.6,
description: 'Решения в области информационных технологий',
slogan: 'Технологии для бизнеса'
},
{
fullName: 'ООО "ТоргПартнер"',
shortName: 'ООО "ТоргПартнер"',
inn: '7707083897',
ogrn: '1027700132199',
legalForm: 'ООО',
industry: 'Оптовая торговля',
companySize: '51-250',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://torgpartner.ru',
verified: true,
rating: 4.3,
description: 'Оптовые поставки и логистика',
slogan: 'Надежный партнер в торговле'
},
{
fullName: 'ООО "ЭнергоПлюс"',
shortName: 'ООО "ЭнергоПлюс"',
inn: '7707083898',
ogrn: '1027700132200',
legalForm: 'ООО',
industry: 'Энергетика',
companySize: '251-500',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://energoplus.ru',
verified: true,
rating: 4.7,
description: 'Энергетические решения и консалтинг',
slogan: 'Энергия для развития'
}
];
for (const mockCompanyData of mockCompanies) {
try {
const existingCompany = await Company.findOne({ inn: mockCompanyData.inn });
if (!existingCompany) {
await Company.create(mockCompanyData);
log(`✅ Mock company created: ${mockCompanyData.fullName}`);
}
} catch (err) {
// Ignore errors for mock company creation - это может быть ошибка аутентификации
log(` Mock company init failed: ${mockCompanyData.fullName}`);
}
} else if (!existingUser.companyId || existingUser.companyId.toString() !== PRESET_COMPANY_ID.toString()) {
existingUser.companyId = PRESET_COMPANY_ID;
existingUser.updatedAt = new Date();
await existingUser.save();
log(' Test user company reference was fixed');
}
} catch (error) {
// Ошибка аутентификации или другие ошибки БД - продолжаем работу
if (error.message && error.message.includes('authentication')) {
log(' Database authentication required - test data initialization deferred');
} else {
console.error('Error initializing test data:', error.message);
console.error('Error initializing test data:', error.message);
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
try {
await connectDB();
} catch (connectError) {
if (process.env.DEV === 'true') {
console.error('Failed to re-connect after auth error:', connectError.message);
}
}
}
}
};
// Пытаемся инициализировать с задержкой (даёт время на подключение)
setTimeout(() => {
initializeTestUser().catch(err => {
log(`⚠️ Deferred test data initialization failed: ${err.message}`);
});
}, 2000);
initializeTestUser();
// Регистрация
router.post('/register', async (req, res) => {
try {
await waitForDatabaseConnection();
const { email, password, firstName, lastName, position, phone, fullName, inn, ogrn, legalForm, industry, companySize, website } = req.body;
// Проверка обязательных полей
@@ -250,6 +322,14 @@ router.post('/register', async (req, res) => {
// Вход
router.post('/login', async (req, res) => {
try {
if (process.env.DEV === 'true') {
console.log('[Auth] /login called');
}
await waitForDatabaseConnection();
if (process.env.DEV === 'true') {
console.log('[Auth] DB ready, running login query');
}
const { email, password } = req.body;
if (!email || !password) {
@@ -266,104 +346,54 @@ router.post('/login', async (req, res) => {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Инициализация других тестовых компаний
const mockCompanies = [
{
fullName: 'ООО "СтройКомплект"',
shortName: 'ООО "СтройКомплект"',
inn: '7707083894',
ogrn: '1027700132196',
legalForm: 'ООО',
industry: 'Строительство',
companySize: '51-250',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://stroykomplekt.ru',
verified: true,
rating: 4.8,
description: 'Компания строит будущее вместе',
slogan: 'Строим будущее вместе'
},
{
fullName: 'АО "Московский Строй"',
shortName: 'АО "Московский Строй"',
inn: '7707083895',
ogrn: '1027700132197',
legalForm: 'АО',
industry: 'Строительство',
companySize: '500+',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://moscow-stroy.ru',
verified: true,
rating: 4.9,
description: 'Качество и надежность с 1995 года',
slogan: 'Качество и надежность'
},
{
fullName: 'ООО "Тероект"',
shortName: 'ООО "Тероект"',
inn: '7707083896',
ogrn: '1027700132198',
legalForm: 'ООО',
industry: 'IT',
companySize: '11-50',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://techproject.ru',
verified: true,
rating: 4.6,
description: 'Решения в области информационных технологий',
slogan: 'Технологии для бизнеса'
},
{
fullName: 'ООО "ТоргПартнер"',
shortName: 'ООО "ТоргПартнер"',
inn: '7707083897',
ogrn: '1027700132199',
legalForm: 'ООО',
industry: 'Оптовая торговля',
companySize: '51-250',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://torgpartner.ru',
verified: true,
rating: 4.3,
description: 'Оптовые поставки и логистика',
slogan: 'Надежный партнер в торговле'
},
{
fullName: 'ООО "ЭнергоПлюс"',
shortName: 'ООО "ЭнергоПлюс"',
inn: '7707083898',
ogrn: '1027700132200',
legalForm: 'ООО',
industry: 'Энергетика',
companySize: '251-500',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://energoplus.ru',
verified: true,
rating: 4.7,
description: 'Энергетические решения и консалтинг',
slogan: 'Энергия для развития'
}
];
for (const mockCompanyData of mockCompanies) {
try {
const existingCompany = await Company.findOne({ inn: mockCompanyData.inn });
if (!existingCompany) {
await Company.create(mockCompanyData);
}
} catch (err) {
// Ignore errors for mock company creation
}
if (
user.email === PRESET_USER_EMAIL &&
(!user.companyId || user.companyId.toString() !== PRESET_COMPANY_ID.toString())
) {
await User.updateOne(
{ _id: user._id },
{ $set: { companyId: PRESET_COMPANY_ID, updatedAt: new Date() } }
);
user.companyId = PRESET_COMPANY_ID;
}
// Получить компанию до использования в generateToken
let companyData = null;
try {
companyData = await Company.findById(user.companyId);
companyData = user.companyId ? await Company.findById(user.companyId) : null;
} catch (err) {
console.error('Failed to fetch company:', err.message);
}
if (user.email === PRESET_USER_EMAIL) {
try {
companyData = await Company.findByIdAndUpdate(
PRESET_COMPANY_ID,
{
$set: {
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
inn: '7707083893',
ogrn: '1027700132195',
legalForm: 'ООО',
industry: 'Производство',
companySize: '50-100',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://test-company.ru',
verified: true,
rating: 4.5,
description: 'Ведущая компания в области производства',
slogan: 'Качество и инновация',
updatedAt: new Date(),
},
},
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
} catch (err) {
console.error('Failed to ensure preset company:', err.message);
}
}
const token = generateToken(user._id.toString(), user.companyId.toString(), user.firstName, user.lastName, companyData?.fullName || 'Company');
log('✅ Token generated for user:', user._id);
@@ -388,14 +418,56 @@ router.post('/login', async (req, res) => {
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: `LOGIN_ERROR: ${error.message}` });
}
});
// Смена пароля
router.post('/change-password', verifyToken, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body || {};
const result = await changePasswordFlow(req.userId, currentPassword, newPassword);
res.status(result.status).json(result.body);
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({ error: error.message });
}
});
// Обновить профиль
router.patch('/profile', (req, res) => {
// требует авторизации, добавить middleware
res.json({ message: 'Update profile endpoint' });
// Удаление аккаунта
router.delete('/account', verifyToken, async (req, res) => {
try {
const { password } = req.body || {};
const result = await deleteAccountFlow(req.userId, password);
res.status(result.status).json(result.body);
} catch (error) {
console.error('Delete account error:', error);
res.status(500).json({ error: error.message });
}
});
// Обновить профиль / универсальные действия
router.patch('/profile', verifyToken, async (req, res) => {
try {
const rawAction = req.body?.action || req.query?.action || req.body?.type;
const payload = req.body?.payload || req.body || {};
const action = typeof rawAction === 'string' ? rawAction : '';
if (action === 'changePassword') {
const result = await changePasswordFlow(req.userId, payload.currentPassword, payload.newPassword);
return res.status(result.status).json(result.body);
}
if (action === 'deleteAccount') {
const result = await deleteAccountFlow(req.userId, payload.password);
return res.status(result.status).json(result.body);
}
res.json({ message: 'Profile endpoint' });
} catch (error) {
console.error('Profile update error:', error);
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -2,6 +2,7 @@ const express = require('express')
const fs = require('fs')
const path = require('path')
const router = express.Router()
const BuyDocument = require('../models/BuyDocument')
// Create remote-assets/docs directory if it doesn't exist
const docsDir = path.join(__dirname, '../../remote-assets/docs')
@@ -9,155 +10,189 @@ if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir, { recursive: true })
}
// In-memory store for documents metadata
const buyDocs = []
function generateId() {
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
}
// GET /buy/docs?ownerCompanyId=...
router.get('/docs', (req, res) => {
const { ownerCompanyId } = req.query
console.log('[BUY API] GET /docs', { ownerCompanyId, totalDocs: buyDocs.length })
let result = buyDocs
if (ownerCompanyId) {
result = result.filter((d) => d.ownerCompanyId === ownerCompanyId)
router.get('/docs', async (req, res) => {
try {
const { ownerCompanyId } = req.query
console.log('[BUY API] GET /docs', { ownerCompanyId })
let query = {}
if (ownerCompanyId) {
query.ownerCompanyId = ownerCompanyId
}
const docs = await BuyDocument.find(query).sort({ createdAt: -1 })
const result = docs.map(doc => ({
...doc.toObject(),
url: `/api/buy/docs/${doc.id}/file`
}))
res.json(result)
} catch (error) {
console.error('[BUY API] Error fetching docs:', error)
res.status(500).json({ error: 'Failed to fetch documents' })
}
result = result.map(doc => ({
...doc,
url: `/api/buy/docs/${doc.id}/file`
}))
res.json(result)
})
// POST /buy/docs
router.post('/docs', (req, res) => {
const { ownerCompanyId, name, type, fileData } = req.body || {}
console.log('[BUY API] POST /docs', { ownerCompanyId, name, type })
if (!ownerCompanyId || !name || !type) {
return res.status(400).json({ error: 'ownerCompanyId, name and type are required' })
}
if (!fileData) {
return res.status(400).json({ error: 'fileData is required' })
}
const id = generateId()
// Save file to disk
router.post('/docs', async (req, res) => {
try {
const { ownerCompanyId, name, type, fileData } = req.body || {}
console.log('[BUY API] POST /docs', { ownerCompanyId, name, type })
if (!ownerCompanyId || !name || !type) {
return res.status(400).json({ error: 'ownerCompanyId, name and type are required' })
}
if (!fileData) {
return res.status(400).json({ error: 'fileData is required' })
}
const id = generateId()
// Save file to disk
const binaryData = Buffer.from(fileData, 'base64')
const filePath = path.join(docsDir, `${id}.${type}`)
fs.writeFileSync(filePath, binaryData)
console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`)
const size = binaryData.length
const url = `/api/buy/docs/${id}/file`
const doc = {
const doc = await BuyDocument.create({
id,
ownerCompanyId,
name,
type,
size,
url,
filePath,
acceptedBy: [],
createdAt: new Date().toISOString(),
}
buyDocs.unshift(doc)
acceptedBy: []
})
console.log('[BUY API] Document created:', id)
res.status(201).json(doc)
res.status(201).json({
...doc.toObject(),
url: `/api/buy/docs/${doc.id}/file`
})
} catch (e) {
console.error(`[BUY API] Error saving file: ${e.message}`)
res.status(500).json({ error: 'Failed to save file' })
}
})
router.post('/docs/:id/accept', (req, res) => {
const { id } = req.params
const { companyId } = req.body || {}
console.log('[BUY API] POST /docs/:id/accept', { id, companyId })
const doc = buyDocs.find((d) => d.id === id)
if (!doc) {
console.log('[BUY API] Document not found:', id)
return res.status(404).json({ error: 'Document not found' })
router.post('/docs/:id/accept', async (req, res) => {
try {
const { id } = req.params
const { companyId } = req.body || {}
console.log('[BUY API] POST /docs/:id/accept', { id, companyId })
if (!companyId) {
return res.status(400).json({ error: 'companyId is required' })
}
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found:', id)
return res.status(404).json({ error: 'Document not found' })
}
if (!doc.acceptedBy.includes(companyId)) {
doc.acceptedBy.push(companyId)
await doc.save()
}
res.json({ id: doc.id, acceptedBy: doc.acceptedBy })
} catch (error) {
console.error('[BUY API] Error accepting document:', error)
res.status(500).json({ error: 'Failed to accept document' })
}
if (!companyId) {
return res.status(400).json({ error: 'companyId is required' })
}
if (!doc.acceptedBy.includes(companyId)) {
doc.acceptedBy.push(companyId)
}
res.json({ id: doc.id, acceptedBy: doc.acceptedBy })
})
router.get('/docs/:id/delete', (req, res) => {
const { id } = req.params
console.log('[BUY API] GET /docs/:id/delete', { id, totalDocs: buyDocs.length })
const index = buyDocs.findIndex((d) => d.id === id)
if (index === -1) {
console.log('[BUY API] Document not found for deletion:', id)
return res.status(404).json({ error: 'Document not found' })
}
const deletedDoc = buyDocs.splice(index, 1)[0]
// Delete file from disk
if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) {
try {
fs.unlinkSync(deletedDoc.filePath)
console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`)
} catch (e) {
console.error(`[BUY API] Error deleting file: ${e.message}`)
router.get('/docs/:id/delete', async (req, res) => {
try {
const { id } = req.params
console.log('[BUY API] GET /docs/:id/delete', { id })
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found for deletion:', id)
return res.status(404).json({ error: 'Document not found' })
}
// Delete file from disk
if (doc.filePath && fs.existsSync(doc.filePath)) {
try {
fs.unlinkSync(doc.filePath)
console.log(`[BUY API] File deleted: ${doc.filePath}`)
} catch (e) {
console.error(`[BUY API] Error deleting file: ${e.message}`)
}
}
await BuyDocument.deleteOne({ id })
console.log('[BUY API] Document deleted via GET:', id)
res.json({ id: doc.id, success: true })
} catch (error) {
console.error('[BUY API] Error deleting document:', error)
res.status(500).json({ error: 'Failed to delete document' })
}
console.log('[BUY API] Document deleted via GET:', id, { remainingDocs: buyDocs.length })
res.json({ id: deletedDoc.id, success: true })
})
router.delete('/docs/:id', (req, res) => {
const { id } = req.params
console.log('[BUY API] DELETE /docs/:id', { id, totalDocs: buyDocs.length })
const index = buyDocs.findIndex((d) => d.id === id)
if (index === -1) {
console.log('[BUY API] Document not found for deletion:', id)
return res.status(404).json({ error: 'Document not found' })
}
const deletedDoc = buyDocs.splice(index, 1)[0]
// Delete file from disk
if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) {
try {
fs.unlinkSync(deletedDoc.filePath)
console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`)
} catch (e) {
console.error(`[BUY API] Error deleting file: ${e.message}`)
router.delete('/docs/:id', async (req, res) => {
try {
const { id } = req.params
console.log('[BUY API] DELETE /docs/:id', { id })
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found for deletion:', id)
return res.status(404).json({ error: 'Document not found' })
}
// Delete file from disk
if (doc.filePath && fs.existsSync(doc.filePath)) {
try {
fs.unlinkSync(doc.filePath)
console.log(`[BUY API] File deleted: ${doc.filePath}`)
} catch (e) {
console.error(`[BUY API] Error deleting file: ${e.message}`)
}
}
await BuyDocument.deleteOne({ id })
console.log('[BUY API] Document deleted:', id)
res.json({ id: doc.id, success: true })
} catch (error) {
console.error('[BUY API] Error deleting document:', error)
res.status(500).json({ error: 'Failed to delete document' })
}
console.log('[BUY API] Document deleted:', id, { remainingDocs: buyDocs.length })
res.json({ id: deletedDoc.id, success: true })
})
// GET /buy/docs/:id/file - Serve the file
router.get('/docs/:id/file', (req, res) => {
const { id } = req.params
console.log('[BUY API] GET /docs/:id/file', { id })
const doc = buyDocs.find(d => d.id === id)
if (!doc) {
console.log('[BUY API] Document not found:', id)
return res.status(404).json({ error: 'Document not found' })
}
const filePath = path.join(docsDir, `${id}.${doc.type}`)
if (!fs.existsSync(filePath)) {
console.log('[BUY API] File not found on disk:', filePath)
return res.status(404).json({ error: 'File not found on disk' })
}
router.get('/docs/:id/file', async (req, res) => {
try {
const { id } = req.params
console.log('[BUY API] GET /docs/:id/file', { id })
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found:', id)
return res.status(404).json({ error: 'Document not found' })
}
const filePath = path.join(docsDir, `${id}.${doc.type}`)
if (!fs.existsSync(filePath)) {
console.log('[BUY API] File not found on disk:', filePath)
return res.status(404).json({ error: 'File not found on disk' })
}
const fileBuffer = fs.readFileSync(filePath)
const mimeTypes = {
@@ -170,7 +205,6 @@ router.get('/docs/:id/file', (req, res) => {
const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_')
res.setHeader('Content-Type', mimeType)
// RFC 5987 encoding: filename for ASCII fallback, filename* for UTF-8 with percent-encoding
const encodedFilename = encodeURIComponent(`${doc.name}.${doc.type}`)
res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}.${doc.type}"; filename*=UTF-8''${encodedFilename}`)
res.setHeader('Content-Length', fileBuffer.length)

View File

@@ -2,6 +2,74 @@ const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const BuyProduct = require('../models/BuyProduct');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const UPLOADS_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'buy-products');
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
ensureDirectory(UPLOADS_ROOT);
const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
const ALLOWED_MIME_TYPES = new Set([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
]);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const productId = req.params.id || 'common';
const productDir = path.join(UPLOADS_ROOT, productId);
ensureDirectory(productDir);
cb(null, productDir);
},
filename: (req, file, cb) => {
const originalExtension = path.extname(file.originalname) || '';
const baseName = path
.basename(file.originalname, originalExtension)
.replace(/[^a-zA-Z0-9-_]+/g, '_')
.toLowerCase();
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: MAX_FILE_SIZE,
},
fileFilter: (req, file, cb) => {
if (ALLOWED_MIME_TYPES.has(file.mimetype)) {
cb(null, true);
return;
}
req.fileValidationError = 'UNSUPPORTED_FILE_TYPE';
cb(null, false);
},
});
const handleSingleFileUpload = (req, res, next) => {
upload.single('file')(req, res, (err) => {
if (err) {
console.error('[BuyProducts] Multer error:', err.message);
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File is too large. Maximum size is 15MB.' });
}
return res.status(400).json({ error: err.message });
}
next();
});
};
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
@@ -43,7 +111,7 @@ router.post('/', verifyToken, async (req, res) => {
try {
const { name, description, quantity, unit, status } = req.body;
log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId });
log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.companyId });
if (!name || !description || !quantity) {
return res.status(400).json({
@@ -58,7 +126,7 @@ router.post('/', verifyToken, async (req, res) => {
}
const newProduct = new BuyProduct({
companyId: req.user.companyId,
companyId: req.companyId,
name: name.trim(),
description: description.trim(),
quantity: quantity.trim(),
@@ -97,7 +165,7 @@ router.put('/:id', verifyToken, async (req, res) => {
}
// Проверить, что товар принадлежит текущей компании
if (product.companyId !== req.user.companyId) {
if (product.companyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
@@ -134,7 +202,7 @@ router.delete('/:id', verifyToken, async (req, res) => {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId.toString() !== req.user.companyId.toString()) {
if (product.companyId.toString() !== req.companyId.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
@@ -153,11 +221,9 @@ router.delete('/:id', verifyToken, async (req, res) => {
});
// POST /buy-products/:id/files - добавить файл к товару
router.post('/:id/files', verifyToken, async (req, res) => {
router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) => {
try {
const { id } = req.params;
const { fileName, fileUrl, fileType, fileSize } = req.body;
const product = await BuyProduct.findById(id);
if (!product) {
@@ -165,23 +231,33 @@ router.post('/:id/files', verifyToken, async (req, res) => {
}
// Только владелец товара может добавить файл
if (product.companyId.toString() !== req.user.companyId.toString()) {
if (product.companyId.toString() !== req.companyId.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
if (req.fileValidationError) {
return res.status(400).json({ error: 'Unsupported file type. Use PDF, DOC, DOCX, XLS, XLSX or CSV.' });
}
if (!req.file) {
return res.status(400).json({ error: 'File is required' });
}
const relativePath = path.join('buy-products', id, req.file.filename).replace(/\\/g, '/');
const file = {
id: 'file-' + Date.now(),
name: fileName,
url: fileUrl,
type: fileType,
size: fileSize,
uploadedAt: new Date()
id: `file-${Date.now()}`,
name: req.file.originalname,
url: `/uploads/${relativePath}`,
type: req.file.mimetype,
size: req.file.size,
uploadedAt: new Date(),
storagePath: relativePath,
};
product.files.push(file);
await product.save();
log('[BuyProducts] File added to product:', id);
log('[BuyProducts] File added to product:', id, file.name);
res.json(product);
} catch (error) {
@@ -204,14 +280,28 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId.toString() !== req.user.companyId.toString()) {
if (product.companyId.toString() !== req.companyId.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
const fileToRemove = product.files.find((f) => f.id === fileId);
if (!fileToRemove) {
return res.status(404).json({ error: 'File not found' });
}
product.files = product.files.filter(f => f.id !== fileId);
await product.save();
log('[BuyProducts] File deleted from product:', id);
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', storedPath);
fs.promises.unlink(absolutePath).catch((unlinkError) => {
if (unlinkError && unlinkError.code !== 'ENOENT') {
console.error('[BuyProducts] Failed to remove file from disk:', unlinkError.message);
}
});
log('[BuyProducts] File deleted from product:', id, fileId);
res.json(product);
} catch (error) {
@@ -227,7 +317,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
router.post('/:id/accept', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const companyId = req.user.companyId;
const companyId = req.companyId;
const product = await BuyProduct.findById(id);

View File

@@ -2,17 +2,10 @@ const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Company = require('../models/Company');
// Инициализация данных при запуске
const initializeCompanies = async () => {
try {
// Уже не нужна инициализация, она производится через authAPI
} catch (error) {
console.error('Error initializing companies:', error);
}
};
initializeCompanies();
const Experience = require('../models/Experience');
const Request = require('../models/Request');
const Message = require('../models/Message');
const { Types } = require('mongoose');
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
router.get('/my/info', verifyToken, async (req, res) => {
@@ -44,23 +37,64 @@ router.get('/my/info', verifyToken, async (req, res) => {
router.get('/my/stats', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const user = await require('../models/User').findById(userId);
if (!user || !user.companyId) {
return res.status(404).json({ error: 'Company not found' });
const User = require('../models/User');
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
let companyId = user.companyId;
if (!companyId) {
const fallbackCompany = await Company.create({
fullName: 'Компания пользователя',
shortName: 'Компания пользователя',
verified: false,
partnerGeography: [],
});
user.companyId = fallbackCompany._id;
user.updatedAt = new Date();
await user.save();
companyId = fallbackCompany._id;
}
let company = await Company.findById(companyId);
if (!company) {
company = await Company.create({
_id: companyId,
fullName: 'Компания пользователя',
verified: false,
partnerGeography: [],
});
}
const companyIdString = company._id.toString();
const companyObjectId = Types.ObjectId.isValid(companyIdString)
? new Types.ObjectId(companyIdString)
: null;
const [sentRequests, receivedRequests, unreadMessages] = await Promise.all([
Request.countDocuments({ senderCompanyId: companyIdString }),
Request.countDocuments({ recipientCompanyId: companyIdString }),
companyObjectId
? Message.countDocuments({ recipientCompanyId: companyObjectId, read: false })
: Promise.resolve(0),
]);
const stats = {
profileViews: Math.floor(Math.random() * 1000),
profileViewsChange: Math.floor(Math.random() * 20 - 10),
sentRequests: Math.floor(Math.random() * 50),
sentRequestsChange: Math.floor(Math.random() * 10 - 5),
receivedRequests: Math.floor(Math.random() * 30),
receivedRequestsChange: Math.floor(Math.random() * 5 - 2),
newMessages: Math.floor(Math.random() * 10),
rating: Math.random() * 5
profileViews: company?.metrics?.profileViews || 0,
profileViewsChange: 0,
sentRequests,
sentRequestsChange: 0,
receivedRequests,
receivedRequestsChange: 0,
newMessages: unreadMessages,
rating: Number.isFinite(company?.rating) ? Number(company.rating) : 0,
};
res.json(stats);
} catch (error) {
console.error('Get company stats error:', error);
@@ -68,15 +102,22 @@ router.get('/my/stats', verifyToken, async (req, res) => {
}
});
// Experience endpoints ДОЛЖНЫ быть ДО получения компании по ID
let companyExperience = [];
// GET /:id/experience - получить опыт компании
router.get('/:id/experience', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const experience = companyExperience.filter(e => e.companyId === id);
res.json(experience);
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const experience = await Experience.find({ companyId: new Types.ObjectId(id) })
.sort({ createdAt: -1 });
res.json(experience.map(exp => ({
...exp.toObject(),
id: exp._id
})));
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -88,23 +129,24 @@ router.post('/:id/experience', verifyToken, async (req, res) => {
const { id } = req.params;
const { confirmed, customer, subject, volume, contact, comment } = req.body;
const expId = Math.random().toString(36).substr(2, 9);
const newExp = {
id: expId,
_id: expId,
companyId: id,
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const newExp = await Experience.create({
companyId: new Types.ObjectId(id),
confirmed: confirmed || false,
customer: customer || '',
subject: subject || '',
volume: volume || '',
contact: contact || '',
comment: comment || '',
createdAt: new Date(),
updatedAt: new Date()
};
comment: comment || ''
});
companyExperience.push(newExp);
res.status(201).json(newExp);
res.status(201).json({
...newExp.toObject(),
id: newExp._id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -114,19 +156,28 @@ router.post('/:id/experience', verifyToken, async (req, res) => {
router.put('/:id/experience/:expId', verifyToken, async (req, res) => {
try {
const { id, expId } = req.params;
const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id);
if (index === -1) {
if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) {
return res.status(400).json({ error: 'Invalid IDs' });
}
const experience = await Experience.findByIdAndUpdate(
new Types.ObjectId(expId),
{
...req.body,
updatedAt: new Date()
},
{ new: true }
);
if (!experience || experience.companyId.toString() !== id) {
return res.status(404).json({ error: 'Experience not found' });
}
companyExperience[index] = {
...companyExperience[index],
...req.body,
updatedAt: new Date()
};
res.json(companyExperience[index]);
res.json({
...experience.toObject(),
id: experience._id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -136,13 +187,18 @@ router.put('/:id/experience/:expId', verifyToken, async (req, res) => {
router.delete('/:id/experience/:expId', verifyToken, async (req, res) => {
try {
const { id, expId } = req.params;
const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id);
if (index === -1) {
if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) {
return res.status(400).json({ error: 'Invalid IDs' });
}
const experience = await Experience.findById(new Types.ObjectId(expId));
if (!experience || experience.companyId.toString() !== id) {
return res.status(404).json({ error: 'Experience not found' });
}
companyExperience.splice(index, 1);
await Experience.findByIdAndDelete(new Types.ObjectId(expId));
res.json({ message: 'Experience deleted' });
} catch (error) {
res.status(500).json({ error: error.message });
@@ -155,7 +211,24 @@ router.get('/:id', async (req, res) => {
const company = await Company.findById(req.params.id);
if (!company) {
return res.status(404).json({ error: 'Company not found' });
if (!Types.ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Company not found' });
}
const placeholder = await Company.create({
_id: new Types.ObjectId(req.params.id),
fullName: 'Новая компания',
shortName: 'Новая компания',
verified: false,
partnerGeography: [],
industry: '',
companySize: '',
});
return res.json({
...placeholder.toObject(),
id: placeholder._id,
});
}
res.json({

View File

@@ -1,12 +1,11 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
// In-memory хранилище для опыта работы (mock)
let experiences = [];
const Experience = require('../models/Experience');
const { Types } = require('mongoose');
// GET /experience - Получить список опыта работы компании
router.get('/', verifyToken, (req, res) => {
router.get('/', verifyToken, async (req, res) => {
try {
const { companyId } = req.query;
@@ -14,8 +13,18 @@ router.get('/', verifyToken, (req, res) => {
return res.status(400).json({ error: 'companyId is required' });
}
const companyExperiences = experiences.filter(exp => exp.companyId === companyId);
res.json(companyExperiences);
if (!Types.ObjectId.isValid(companyId)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const companyExperiences = await Experience.find({
companyId: new Types.ObjectId(companyId)
}).sort({ createdAt: -1 });
res.json(companyExperiences.map(exp => ({
...exp.toObject(),
id: exp._id
})));
} catch (error) {
console.error('Get experience error:', error);
res.status(500).json({ error: 'Internal server error' });
@@ -23,7 +32,7 @@ router.get('/', verifyToken, (req, res) => {
});
// POST /experience - Создать запись опыта работы
router.post('/', verifyToken, (req, res) => {
router.post('/', verifyToken, async (req, res) => {
try {
const { companyId, data } = req.body;
@@ -31,28 +40,30 @@ router.post('/', verifyToken, (req, res) => {
return res.status(400).json({ error: 'companyId and data are required' });
}
if (!Types.ObjectId.isValid(companyId)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const { confirmed, customer, subject, volume, contact, comment } = data;
if (!customer || !subject) {
return res.status(400).json({ error: 'customer and subject are required' });
}
const newExperience = {
id: `exp-${Date.now()}`,
companyId,
const newExperience = await Experience.create({
companyId: new Types.ObjectId(companyId),
confirmed: confirmed || false,
customer,
subject,
volume: volume || '',
contact: contact || '',
comment: comment || '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
comment: comment || ''
});
experiences.push(newExperience);
res.status(201).json(newExperience);
res.status(201).json({
...newExperience.toObject(),
id: newExperience._id
});
} catch (error) {
console.error('Create experience error:', error);
res.status(500).json({ error: 'Internal server error' });
@@ -60,7 +71,7 @@ router.post('/', verifyToken, (req, res) => {
});
// PUT /experience/:id - Обновить запись опыта работы
router.put('/:id', verifyToken, (req, res) => {
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { data } = req.body;
@@ -69,21 +80,27 @@ router.put('/:id', verifyToken, (req, res) => {
return res.status(400).json({ error: 'data is required' });
}
const index = experiences.findIndex(exp => exp.id === id);
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid experience ID' });
}
if (index === -1) {
const updatedExperience = await Experience.findByIdAndUpdate(
new Types.ObjectId(id),
{
...data,
updatedAt: new Date()
},
{ new: true }
);
if (!updatedExperience) {
return res.status(404).json({ error: 'Experience not found' });
}
const updatedExperience = {
...experiences[index],
...data,
updatedAt: new Date().toISOString()
};
experiences[index] = updatedExperience;
res.json(updatedExperience);
res.json({
...updatedExperience.toObject(),
id: updatedExperience._id
});
} catch (error) {
console.error('Update experience error:', error);
res.status(500).json({ error: 'Internal server error' });
@@ -91,17 +108,19 @@ router.put('/:id', verifyToken, (req, res) => {
});
// DELETE /experience/:id - Удалить запись опыта работы
router.delete('/:id', verifyToken, (req, res) => {
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const index = experiences.findIndex(exp => exp.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Experience not found' });
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid experience ID' });
}
experiences.splice(index, 1);
const deletedExperience = await Experience.findByIdAndDelete(new Types.ObjectId(id));
if (!deletedExperience) {
return res.status(404).json({ error: 'Experience not found' });
}
res.json({ message: 'Experience deleted successfully' });
} catch (error) {

View File

@@ -1,16 +1,49 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const BuyProduct = require('../models/BuyProduct');
const Request = require('../models/Request');
// Получить агрегированные данные для главной страницы
router.get('/aggregates', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({
docsCount: 0,
acceptsCount: 0,
requestsCount: 0
});
}
const companyId = user.companyId.toString();
const [docsCount, acceptsCount, requestsCount] = await Promise.all([
BuyProduct.countDocuments({ companyId }),
Request.countDocuments({
$or: [
{ senderCompanyId: companyId, status: 'accepted' },
{ recipientCompanyId: companyId, status: 'accepted' }
]
}),
Request.countDocuments({
$or: [
{ senderCompanyId: companyId },
{ recipientCompanyId: companyId }
]
})
]);
res.json({
docsCount: 0,
acceptsCount: 0,
requestsCount: 0
docsCount,
acceptsCount,
requestsCount
});
} catch (error) {
console.error('Error getting aggregates:', error);
res.status(500).json({ error: error.message });
}
});
@@ -18,17 +51,42 @@ router.get('/aggregates', verifyToken, async (req, res) => {
// Получить статистику компании
router.get('/stats', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const Company = require('../models/Company');
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({
profileViews: 0,
profileViewsChange: 0,
sentRequests: 0,
sentRequestsChange: 0,
receivedRequests: 0,
receivedRequestsChange: 0,
newMessages: 0,
rating: 0
});
}
const companyId = user.companyId.toString();
const company = await Company.findById(user.companyId);
const sentRequests = await Request.countDocuments({ senderCompanyId: companyId });
const receivedRequests = await Request.countDocuments({ recipientCompanyId: companyId });
res.json({
profileViews: 12,
profileViewsChange: 5,
sentRequests: 3,
sentRequestsChange: 1,
receivedRequests: 7,
receivedRequestsChange: 2,
newMessages: 4,
rating: 4.5
profileViews: company?.metrics?.profileViews || 0,
profileViewsChange: 0,
sentRequests,
sentRequestsChange: 0,
receivedRequests,
receivedRequestsChange: 0,
newMessages: 0,
rating: company?.rating || 0
});
} catch (error) {
console.error('Error getting stats:', error);
res.status(500).json({ error: error.message });
}
});
@@ -36,11 +94,40 @@ router.get('/stats', verifyToken, async (req, res) => {
// Получить рекомендации партнеров (AI)
router.get('/recommendations', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const Company = require('../models/Company');
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({
recommendations: [],
message: 'No recommendations available'
});
}
// Получить компании кроме текущей
const companies = await Company.find({
_id: { $ne: user.companyId }
})
.sort({ rating: -1 })
.limit(5);
const recommendations = companies.map(company => ({
id: company._id.toString(),
name: company.fullName || company.shortName,
industry: company.industry,
logo: company.logo,
matchScore: company.rating ? Math.min(100, Math.round(company.rating * 20)) : 50,
reason: 'Matches your industry'
}));
res.json({
recommendations: [],
message: 'No recommendations available yet'
recommendations,
message: recommendations.length > 0 ? 'Recommendations available' : 'No recommendations available'
});
} catch (error) {
console.error('Error getting recommendations:', error);
res.status(500).json({ error: error.message });
}
});

View File

@@ -17,7 +17,7 @@ const log = (message, data = '') => {
// GET /messages/threads - получить все потоки для компании
router.get('/threads', verifyToken, async (req, res) => {
try {
const companyId = req.user.companyId;
const companyId = req.companyId;
const { ObjectId } = require('mongoose').Types;
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
@@ -91,7 +91,7 @@ router.get('/threads', verifyToken, async (req, res) => {
router.get('/:threadId', verifyToken, async (req, res) => {
try {
const { threadId } = req.params;
const companyId = req.user.companyId;
const companyId = req.companyId;
// Получить все сообщения потока
const threadMessages = await Message.find({ threadId })
@@ -128,7 +128,7 @@ router.post('/:threadId', verifyToken, async (req, res) => {
const threadParts = threadId.replace('thread-', '').split('-');
let recipientCompanyId = null;
const currentSender = senderCompanyId || req.user.companyId;
const currentSender = senderCompanyId || req.companyId;
const currentSenderString = currentSender.toString ? currentSender.toString() : currentSender;
if (threadParts.length >= 2) {

View File

@@ -28,7 +28,7 @@ const transformProduct = (doc) => {
// GET /products - Получить список продуктов/услуг компании (текущего пользователя)
router.get('/', verifyToken, async (req, res) => {
try {
const companyId = req.user.companyId;
const companyId = req.companyId;
log('[Products] GET Fetching products for companyId:', companyId);
@@ -48,7 +48,7 @@ router.get('/', verifyToken, async (req, res) => {
router.post('/', verifyToken, async (req, res) => {
// try {
const { name, category, description, type, productUrl, price, unit, minOrder } = req.body;
const companyId = req.user.companyId;
const companyId = req.companyId;
log('[Products] POST Creating product:', { name, category, type });
@@ -88,7 +88,7 @@ router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const companyId = req.user.companyId;
const companyId = req.companyId;
const product = await Product.findById(id);
@@ -120,7 +120,7 @@ router.patch('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const companyId = req.user.companyId;
const companyId = req.companyId;
const product = await Product.findById(id);
@@ -150,7 +150,7 @@ router.patch('/:id', verifyToken, async (req, res) => {
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const companyId = req.user.companyId;
const companyId = req.companyId;
const product = await Product.findById(id);

View File

@@ -2,6 +2,10 @@ const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Request = require('../models/Request');
const BuyProduct = require('../models/BuyProduct');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
@@ -14,10 +18,166 @@ const log = (message, data = '') => {
}
};
const REQUESTS_UPLOAD_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'requests');
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
ensureDirectory(REQUESTS_UPLOAD_ROOT);
const MAX_REQUEST_FILE_SIZE = 20 * 1024 * 1024; // 20MB
const ALLOWED_REQUEST_MIME_TYPES = new Set([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
]);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const subfolder = req.requestUploadSubfolder || '';
const destinationDir = path.join(REQUESTS_UPLOAD_ROOT, subfolder);
ensureDirectory(destinationDir);
cb(null, destinationDir);
},
filename: (req, file, cb) => {
const extension = path.extname(file.originalname) || '';
const baseName = path
.basename(file.originalname, extension)
.replace(/[^a-zA-Z0-9-_]+/g, '_')
.toLowerCase();
cb(null, `${Date.now()}_${baseName}${extension}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: MAX_REQUEST_FILE_SIZE,
},
fileFilter: (req, file, cb) => {
if (ALLOWED_REQUEST_MIME_TYPES.has(file.mimetype)) {
cb(null, true);
return;
}
if (!req.invalidFiles) {
req.invalidFiles = [];
}
req.invalidFiles.push(file.originalname);
cb(null, false);
},
});
const handleFilesUpload = (fieldName, subfolderResolver, maxCount = 10) => (req, res, next) => {
req.invalidFiles = [];
req.requestUploadSubfolder = subfolderResolver(req);
upload.array(fieldName, maxCount)(req, res, (err) => {
if (err) {
console.error('[Requests] Multer error:', err.message);
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File is too large. Maximum size is 20MB.' });
}
return res.status(400).json({ error: err.message });
}
next();
});
};
const cleanupUploadedFiles = async (req) => {
if (!Array.isArray(req.files) || req.files.length === 0) {
return;
}
const subfolder = req.requestUploadSubfolder || '';
const removalTasks = req.files.map((file) => {
const filePath = path.join(REQUESTS_UPLOAD_ROOT, subfolder, file.filename);
return fs.promises.unlink(filePath).catch((error) => {
if (error.code !== 'ENOENT') {
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
}
});
});
await Promise.all(removalTasks);
};
const mapFilesToMetadata = (req) => {
if (!Array.isArray(req.files) || req.files.length === 0) {
return [];
}
const subfolder = req.requestUploadSubfolder || '';
return req.files.map((file) => {
const relativePath = path.join('requests', subfolder, file.filename).replace(/\\/g, '/');
return {
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.originalname,
url: `/uploads/${relativePath}`,
type: file.mimetype,
size: file.size,
uploadedAt: new Date(),
storagePath: relativePath,
};
});
};
const normalizeToArray = (value) => {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
// ignore JSON parse errors
}
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
};
const removeStoredFiles = async (files = []) => {
if (!files || files.length === 0) {
return;
}
const tasks = files
.filter((file) => file && file.storagePath)
.map((file) => {
const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', file.storagePath);
return fs.promises.unlink(absolutePath).catch((error) => {
if (error.code !== 'ENOENT') {
console.error('[Requests] Failed to remove stored file:', error.message);
}
});
});
await Promise.all(tasks);
};
// GET /requests/sent - получить отправленные запросы
router.get('/sent', verifyToken, async (req, res) => {
try {
const companyId = req.user.companyId;
const companyId = req.companyId;
if (!companyId) {
return res.status(400).json({ error: 'Company ID is required' });
}
const requests = await Request.find({ senderCompanyId: companyId })
.sort({ createdAt: -1 })
@@ -35,7 +195,11 @@ router.get('/sent', verifyToken, async (req, res) => {
// GET /requests/received - получить полученные запросы
router.get('/received', verifyToken, async (req, res) => {
try {
const companyId = req.user.companyId;
const companyId = req.companyId;
if (!companyId) {
return res.status(400).json({ error: 'Company ID is required' });
}
const requests = await Request.find({ recipientCompanyId: companyId })
.sort({ createdAt: -1 })
@@ -51,95 +215,164 @@ router.get('/received', verifyToken, async (req, res) => {
});
// POST /requests - создать запрос
router.post('/', verifyToken, async (req, res) => {
try {
const { text, recipientCompanyIds, productId, files } = req.body;
const senderCompanyId = req.user.companyId;
router.post(
'/',
verifyToken,
handleFilesUpload('files', (req) => path.join('sent', (req.companyId || 'unknown').toString()), 10),
async (req, res) => {
try {
const senderCompanyId = req.companyId;
const recipients = normalizeToArray(req.body.recipientCompanyIds);
const text = (req.body.text || '').trim();
const productId = req.body.productId ? String(req.body.productId) : null;
let subject = (req.body.subject || '').trim();
if (!text || !recipientCompanyIds || !Array.isArray(recipientCompanyIds) || recipientCompanyIds.length === 0) {
return res.status(400).json({ error: 'text and recipientCompanyIds array required' });
}
// Отправить запрос каждой компании
const results = [];
for (const recipientCompanyId of recipientCompanyIds) {
try {
const request = new Request({
senderCompanyId,
recipientCompanyId,
text,
productId,
files: files || [],
status: 'pending'
});
await request.save();
results.push({
companyId: recipientCompanyId,
success: true,
message: 'Request sent successfully'
});
log('[Requests] Request sent to company:', recipientCompanyId);
} catch (err) {
results.push({
companyId: recipientCompanyId,
success: false,
message: err.message
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
return res.status(400).json({
error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.',
details: req.invalidFiles,
});
}
if (!text) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Request text is required' });
}
if (!recipients.length) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'At least one recipient is required' });
}
if (!subject && productId) {
try {
const product = await BuyProduct.findById(productId);
if (product) {
subject = product.name;
}
} catch (lookupError) {
console.error('[Requests] Failed to lookup product for subject:', lookupError.message);
}
}
if (!subject) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Subject is required' });
}
const uploadedFiles = mapFilesToMetadata(req);
const results = [];
for (const recipientCompanyId of recipients) {
try {
const request = new Request({
senderCompanyId,
recipientCompanyId,
text,
productId,
subject,
files: uploadedFiles,
responseFiles: [],
status: 'pending',
});
await request.save();
results.push({
companyId: recipientCompanyId,
success: true,
message: 'Request sent successfully',
});
log('[Requests] Request sent to company:', recipientCompanyId);
} catch (err) {
console.error('[Requests] Error storing request for company:', recipientCompanyId, err.message);
results.push({
companyId: recipientCompanyId,
success: false,
message: err.message,
});
}
}
const createdAt = new Date();
res.status(201).json({
id: 'bulk-' + Date.now(),
text,
subject,
productId,
files: uploadedFiles,
result: results,
createdAt,
});
} catch (error) {
console.error('[Requests] Error creating request:', error.message);
res.status(500).json({ error: error.message });
}
// Сохранить отчет
const report = {
text,
result: results,
createdAt: new Date()
};
res.status(201).json({
id: 'bulk-' + Date.now(),
...report,
files: files || []
});
} catch (error) {
console.error('[Requests] Error creating request:', error.message);
res.status(500).json({ error: error.message });
}
});
);
// PUT /requests/:id - ответить на запрос
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { response, status } = req.body;
router.put(
'/:id',
verifyToken,
handleFilesUpload('responseFiles', (req) => path.join('responses', req.params.id || 'unknown'), 5),
async (req, res) => {
try {
const { id } = req.params;
const responseText = (req.body.response || '').trim();
const statusRaw = (req.body.status || 'accepted').toLowerCase();
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
const request = await Request.findById(id);
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
return res.status(400).json({
error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.',
details: req.invalidFiles,
});
}
if (!request) {
return res.status(404).json({ error: 'Request not found' });
if (!responseText) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Response text is required' });
}
const request = await Request.findById(id);
if (!request) {
await cleanupUploadedFiles(req);
return res.status(404).json({ error: 'Request not found' });
}
if (request.recipientCompanyId !== req.companyId) {
await cleanupUploadedFiles(req);
return res.status(403).json({ error: 'Not authorized' });
}
const uploadedResponseFiles = mapFilesToMetadata(req);
if (uploadedResponseFiles.length > 0) {
await removeStoredFiles(request.responseFiles || []);
request.responseFiles = uploadedResponseFiles;
}
request.response = responseText;
request.status = status;
request.respondedAt = new Date();
request.updatedAt = new Date();
await request.save();
log('[Requests] Request responded:', id);
res.json(request);
} catch (error) {
console.error('[Requests] Error responding to request:', error.message);
res.status(500).json({ error: error.message });
}
// Только получатель может ответить на запрос
if (request.recipientCompanyId !== req.user.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
request.response = response;
request.status = status || 'accepted';
request.respondedAt = new Date();
request.updatedAt = new Date();
await request.save();
log('[Requests] Request responded:', id);
res.json(request);
} catch (error) {
console.error('[Requests] Error responding to request:', error.message);
res.status(500).json({ error: error.message });
}
});
);
// DELETE /requests/:id - удалить запрос
router.delete('/:id', verifyToken, async (req, res) => {
@@ -153,10 +386,13 @@ router.delete('/:id', verifyToken, async (req, res) => {
}
// Может удалить отправитель или получатель
if (request.senderCompanyId !== req.user.companyId && request.recipientCompanyId !== req.user.companyId) {
if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
await removeStoredFiles(request.files || []);
await removeStoredFiles(request.responseFiles || []);
await Request.findByIdAndDelete(id);
log('[Requests] Request deleted:', id);

View File

@@ -61,7 +61,7 @@ router.post('/', verifyToken, async (req, res) => {
// Создать новый отзыв
const newReview = new Review({
companyId,
authorCompanyId: req.user.companyId,
authorCompanyId: req.companyId,
authorName: req.user.firstName + ' ' + req.user.lastName,
authorCompany: req.user.companyName || 'Company',
rating: parseInt(rating),

View File

@@ -127,14 +127,8 @@ router.get('/', verifyToken, async (req, res) => {
log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries);
if (dbIndustries.length > 0) {
// Handle both string and array industry values
filters.push({
$or: [
{ industry: { $in: dbIndustries } },
{ industry: { $elemMatch: { $in: dbIndustries } } }
]
});
log('[Search] Added industry filter:', { $or: [{ industry: { $in: dbIndustries } }, { industry: { $elemMatch: { $in: dbIndustries } } }] });
filters.push({ industry: { $in: dbIndustries } });
log('[Search] Added industry filter:', { industry: { $in: dbIndustries } });
} else {
log('[Search] No industries mapped! Codes were:', industryList);
}
@@ -219,10 +213,8 @@ router.get('/', verifyToken, async (req, res) => {
page: pageNum,
totalPages: Math.ceil(total / limitNum),
_debug: {
requestParams: { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder },
filter: JSON.stringify(filter),
filtersCount: filters.length,
appliedFilters: filters.map(f => JSON.stringify(f))
industriesReceived: industries
}
});
} catch (error) {

View File

@@ -1,74 +0,0 @@
const mongoose = require('mongoose');
const { migrateCompanies } = require('./migrate-companies');
require('dotenv').config({ path: '../../.env' });
const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
// Migration history model
const migrationSchema = new mongoose.Schema({
name: { type: String, unique: true, required: true },
executedAt: { type: Date, default: Date.now },
status: { type: String, enum: ['completed', 'failed'], default: 'completed' },
message: String
}, { collection: 'migrations' });
const Migration = mongoose.model('Migration', migrationSchema);
async function initializeDatabase() {
try {
console.log('[Init] Connecting to MongoDB...');
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
console.log('[Init] Connected to MongoDB\n');
// Check if migrations already ran
const migrateCompaniesRan = await Migration.findOne({ name: 'migrate-companies' });
if (!migrateCompaniesRan) {
console.log('[Init] Running migrate-companies migration...');
try {
await migrateCompanies();
// Record successful migration
await Migration.create({
name: 'migrate-companies',
status: 'completed',
message: 'Company data migration completed successfully'
});
console.log('[Init] ✅ migrate-companies recorded in database\n');
} catch (err) {
// Record failed migration
await Migration.create({
name: 'migrate-companies',
status: 'failed',
message: err.message
});
console.error('[Init] ❌ migrate-companies failed:', err.message);
}
} else {
console.log('[Init] migrate-companies already executed:', migrateCompaniesRan.executedAt);
console.log('[Init] Skipping migration...\n');
}
await mongoose.connection.close();
console.log('[Init] Database initialization complete\n');
} catch (err) {
console.error('[Init] ❌ Error during database initialization:', err.message);
process.exit(1);
}
}
module.exports = initializeDatabase;
// Run directly if called as script
if (require.main === module) {
initializeDatabase().catch(err => {
console.error('Initialization failed:', err);
process.exit(1);
});
}

View File

@@ -1,124 +0,0 @@
const mongoose = require('mongoose');
const Company = require('../models/Company');
require('dotenv').config({ path: '../../.env' });
const industryMap = {
'it': 'IT',
'finance': 'Финансы',
'manufacturing': 'Производство',
'construction': 'Строительство',
'retail': 'Розничная торговля',
'wholesale': 'Оптовая торговля',
'logistics': 'Логистика',
'healthcare': 'Здравоохранение',
'education': 'Образование',
'consulting': 'Консалтинг',
'marketing': 'Маркетинг',
'realestate': 'Недвижимость',
'food': 'Пищевая промышленность',
'agriculture': 'Сельское хозяйство',
'energy': 'Энергетика',
'telecom': 'Телекоммуникации',
'media': 'Медиа',
'tourism': 'Туризм',
'legal': 'Юридические услуги',
'other': 'Другое'
};
const validIndustries = Object.values(industryMap);
const industryAliases = {
'Торговля': 'Розничная торговля',
'торговля': 'Розничная торговля',
'Trade': 'Розничная торговля'
};
async function migrateCompanies() {
try {
const allCompanies = await Company.find().exec();
console.log(`[Migration] Found ${allCompanies.length} companies to process`);
let fixedCount = 0;
let errorCount = 0;
for (const company of allCompanies) {
let needsUpdate = false;
let updates = {};
// Check and fix industry field
if (company.industry) {
if (Array.isArray(company.industry)) {
console.log(`[FIX] ${company.fullName}: industry is array, converting to string`);
updates.industry = company.industry[0] || 'Другое';
needsUpdate = true;
} else if (!validIndustries.includes(company.industry)) {
const mapped = industryAliases[company.industry];
if (mapped) {
console.log(`[FIX] ${company.fullName}: "${company.industry}" → "${mapped}"`);
updates.industry = mapped;
needsUpdate = true;
} else {
console.log(`[WARN] ${company.fullName}: unknown industry "${company.industry}"`);
}
}
}
// Check and fix companySize field
if (company.companySize && Array.isArray(company.companySize)) {
console.log(`[FIX] ${company.fullName}: companySize is array, converting to string`);
updates.companySize = company.companySize[0] || '';
needsUpdate = true;
}
if (needsUpdate) {
try {
await Company.updateOne({ _id: company._id }, { $set: updates });
fixedCount++;
console.log(` ✅ Updated`);
} catch (err) {
console.error(` ❌ Error: ${err.message}`);
errorCount++;
}
}
}
console.log('\n[Migration] === MIGRATION SUMMARY ===');
console.log(`[Migration] Total companies: ${allCompanies.length}`);
console.log(`[Migration] Fixed: ${fixedCount}`);
console.log(`[Migration] Errors: ${errorCount}`);
if (fixedCount === 0 && errorCount === 0) {
console.log('[Migration] ✅ No migration needed - all data is valid!');
} else if (errorCount === 0) {
console.log('[Migration] ✅ Migration completed successfully!');
} else {
console.log('[Migration] ⚠️ Migration completed with errors.');
}
} catch (err) {
console.error('[Migration] ❌ Error:', err.message);
throw err;
}
}
module.exports = {
migrateCompanies: migrateCompanies
};
// Run directly if called as script
if (require.main === module) {
const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
}).then(async () => {
console.log('[Migration] Connected to MongoDB\n');
await migrateCompanies();
await mongoose.connection.close();
}).catch(err => {
console.error('[Migration] ❌ Error:', err.message);
process.exit(1);
});
}

View File

@@ -6,17 +6,14 @@ const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procureme
async function migrateMessages() {
try {
// Check if connection exists, if not connect
if (mongoose.connection.readyState === 0) {
console.log('[Migration] Connecting to MongoDB...');
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
console.log('[Migration] Connected to MongoDB');
}
console.log('[Migration] Connecting to MongoDB...');
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
console.log('[Migration] Connected to MongoDB');
// Найти все сообщения
const allMessages = await Message.find().exec();
@@ -84,18 +81,13 @@ async function migrateMessages() {
console.log('[Migration] ✅ Migration completed!');
console.log('[Migration] Fixed:', fixedCount, 'messages');
console.log('[Migration] Errors:', errorCount);
await mongoose.connection.close();
console.log('[Migration] Disconnected from MongoDB');
} catch (err) {
console.error('[Migration] ❌ Error:', err.message);
throw err;
process.exit(1);
}
}
module.exports = { migrateMessages };
// Run directly if called as script
if (require.main === module) {
migrateMessages().catch(err => {
console.error('Migration failed:', err);
process.exit(1);
});
}
migrateMessages();

View File

@@ -1,32 +1,62 @@
const mongoose = require('mongoose');
const path = require('path');
require('dotenv').config();
// Импорт моделей
const User = require('../models/User');
const Company = require('../models/Company');
const User = require(path.join(__dirname, '..', 'models', 'User'));
const Company = require(path.join(__dirname, '..', 'models', 'Company'));
const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
const fallbackUri =
process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
const connectWithFallback = async () => {
try {
console.log('\n📡 Подключение к MongoDB (PRIMARY)...');
await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true });
console.log('✅ Подключено к PRIMARY MongoDB');
} catch (primaryError) {
console.error('❌ Ошибка PRIMARY подключения:', primaryError.message);
const requiresFallback =
primaryError.code === 18 || primaryError.code === 13 || String(primaryError.message || '').includes('auth');
if (!requiresFallback) {
throw primaryError;
}
console.log('\n📡 Подключение к MongoDB (FALLBACK)...');
await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true });
console.log('✅ Подключено к FALLBACK MongoDB');
}
};
const recreateTestUser = async () => {
try {
console.log('[Migration] Processing test user creation...');
await connectWithFallback();
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
const presetUserEmail = 'admin@test-company.ru';
// Удалить старого тестового пользователя
console.log('[Migration] Removing old test user...');
const oldUser = await User.findOne({ email: 'admin@test-company.ru' });
console.log('🗑️ Удаление старого тестового пользователя...');
const oldUser = await User.findOne({ email: presetUserEmail });
if (oldUser) {
// Удалить связанную компанию
if (oldUser.companyId) {
await Company.findByIdAndDelete(oldUser.companyId);
console.log('[Migration] ✓ Old company removed');
console.log(' ✓ Старая компания удалена');
}
await User.findByIdAndDelete(oldUser._id);
console.log('[Migration] ✓ Old user removed');
console.log(' ✓ Старый пользователь удален');
} else {
console.log('[Migration] Old user not found');
console.log(' Старый пользователь не найден');
}
// Создать новую компанию с правильной кодировкой UTF-8
console.log('[Migration] Creating test company...');
console.log('\n🏢 Создание тестовой компании...');
const company = await Company.create({
_id: presetCompanyId,
fullName: 'ООО "Тестовая Компания"',
inn: '1234567890',
ogrn: '1234567890123',
@@ -40,12 +70,12 @@ const recreateTestUser = async () => {
reviewsCount: 10,
dealsCount: 25,
});
console.log('[Migration] ✓ Company created:', company.fullName);
console.log(' ✓ Компания создана:', company.fullName);
// Создать нового пользователя с правильной кодировкой UTF-8
console.log('[Migration] Creating test user...');
console.log('\n👤 Создание тестового пользователя...');
const user = await User.create({
email: 'admin@test-company.ru',
email: presetUserEmail,
password: 'SecurePass123!',
firstName: 'Иван',
lastName: 'Иванов',
@@ -53,10 +83,24 @@ const recreateTestUser = async () => {
phone: '+7 (999) 123-45-67',
companyId: company._id,
});
console.log('[Migration] ✓ User created:', user.firstName, user.lastName);
console.log(' ✓ Пользователь создан:', user.firstName, user.lastName);
// Проверка что данные сохранены правильно
console.log('\n✅ Проверка данных:');
console.log(' Email:', user.email);
console.log(' Имя:', user.firstName);
console.log(' Фамилия:', user.lastName);
console.log(' Компания:', company.fullName);
console.log(' Должность:', user.position);
console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8');
console.log('\n📋 Данные для входа:');
console.log(' Email: admin@test-company.ru');
console.log(' Пароль: SecurePass123!');
console.log('');
// Обновить существующие mock компании
console.log('[Migration] Updating existing companies...');
console.log('\n🔄 Обновление существующих mock компаний...');
const updates = [
{ inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } },
{ inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } },
@@ -67,33 +111,18 @@ const recreateTestUser = async () => {
for (const item of updates) {
await Company.updateOne({ inn: item.inn }, { $set: item.updates });
console.log(`[Migration] ✓ Company updated: INN ${item.inn}`);
console.log(` ✓ Компания обновлена: INN ${item.inn}`);
}
console.log('[Migration] ✅ Test user migration completed!');
await mongoose.connection.close();
process.exit(0);
} catch (error) {
console.error('[Migration] ❌ Error:', error.message);
throw error;
console.error('\n❌ Ошибка:', error.message);
console.error(error);
process.exit(1);
}
};
module.exports = { recreateTestUser };
// Run directly if called as script
if (require.main === module) {
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(async () => {
console.log('[Migration] Connected to MongoDB\n');
await recreateTestUser();
await mongoose.connection.close();
process.exit(0);
}).catch(err => {
console.error('[Migration] ❌ Error:', err.message);
process.exit(1);
});
}
// Запуск
recreateTestUser();

View File

@@ -1,117 +0,0 @@
const mongoose = require('mongoose');
const { migrateCompanies } = require('./migrate-companies');
const { migrateMessages } = require('./migrate-messages');
const { recreateTestUser } = require('./recreate-test-user');
require('dotenv').config();
const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
// Migration history model
const migrationSchema = new mongoose.Schema({
name: { type: String, unique: true, required: true },
executedAt: { type: Date, default: Date.now },
status: { type: String, enum: ['completed', 'failed'], default: 'completed' },
message: String
}, { collection: 'migrations' });
const Migration = mongoose.model('Migration', migrationSchema);
const migrations = [
{ name: 'migrate-companies', fn: migrateCompanies },
{ name: 'migrate-messages', fn: migrateMessages },
{ name: 'recreate-test-user', fn: recreateTestUser }
];
async function runMigrations(shouldCloseConnection = false) {
let mongooseConnected = false;
try {
console.log('\n' + '='.repeat(60));
console.log('🚀 Starting Database Migrations');
console.log('='.repeat(60) + '\n');
// Only connect if not already connected
if (mongoose.connection.readyState === 0) {
console.log('[Migrations] Connecting to MongoDB...');
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
mongooseConnected = true;
console.log('[Migrations] ✅ Connected to MongoDB\n');
} else {
console.log('[Migrations] ✅ Using existing MongoDB connection\n');
}
for (const migration of migrations) {
console.log(`[${migration.name}] Starting...`);
try {
// Check if already executed
const existing = await Migration.findOne({ name: migration.name });
if (existing) {
console.log(`[${migration.name}] Already executed at: ${existing.executedAt.toISOString()}`);
console.log(`[${migration.name}] Status: ${existing.status}`);
if (existing.message) console.log(`[${migration.name}] Message: ${existing.message}`);
console.log('');
continue;
}
// Run migration
await migration.fn();
// Record successful migration
await Migration.create({
name: migration.name,
status: 'completed',
message: `${migration.name} executed successfully`
});
console.log(`[${migration.name}] ✅ Completed and recorded\n`);
} catch (error) {
console.error(`[${migration.name}] ❌ Error: ${error.message}\n`);
// Record failed migration
try {
await Migration.create({
name: migration.name,
status: 'failed',
message: error.message
});
} catch (recordErr) {
// Ignore if we can't record the failure
}
}
}
console.log('='.repeat(60));
console.log('✅ All migrations processed');
console.log('='.repeat(60) + '\n');
} catch (error) {
console.error('\n❌ Fatal migration error:', error.message);
console.error(error);
if (shouldCloseConnection) {
process.exit(1);
}
} finally {
// Only close connection if we created it and requested to close
if (mongooseConnected && shouldCloseConnection) {
await mongoose.connection.close();
console.log('[Migrations] Disconnected from MongoDB\n');
}
}
}
module.exports = { runMigrations, Migration };
// Run directly if called as script
if (require.main === module) {
runMigrations(true).catch(err => {
console.error('Migration failed:', err);
process.exit(1);
});
}

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Скрипт для тестирования логирования
*
* Использование:
* node stubs/scripts/test-logging.js # Логи скрыты (DEV не установлена)
* DEV=true node stubs/scripts/test-logging.js # Логи видны
*/
// Функция логирования из маршрутов
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
console.log('');
console.log('='.repeat(60));
console.log('TEST: Логирование с переменной окружения DEV');
console.log('='.repeat(60));
console.log('');
console.log('Значение DEV:', process.env.DEV || '(не установлена)');
console.log('');
// Тестируем различные логи
log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796');
log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799');
log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796');
log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799');
log('[Products] Found', 1, 'products');
log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796');
log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796');
log('[Messages] Found', 4, 'messages for company');
log('[Messages] Returned', 3, 'unique threads');
log('[Search] Request params:', { query: '', page: 1 });
console.log('');
console.log('='.repeat(60));
console.log('РЕЗУЛЬТАТ:');
console.log('='.repeat(60));
if (process.env.DEV === 'true') {
console.log('✅ DEV=true - логи ВИДНЫ выше');
} else {
console.log('❌ DEV не установлена или != "true" - логи СКРЫТЫ');
console.log('');
console.log('Для включения логов запустите:');
console.log(' export DEV=true && npm start (Linux/Mac)');
console.log(' $env:DEV = "true"; npm start (PowerShell)');
console.log(' set DEV=true && npm start (CMD)');
}
console.log('');
console.log('='.repeat(60));
console.log('');

View File

@@ -1,93 +0,0 @@
const mongoose = require('mongoose');
const Company = require('../models/Company');
require('dotenv').config({ path: '../../.env' });
const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
const industryMap = {
'it': 'IT',
'finance': 'Финансы',
'manufacturing': 'Производство',
'construction': 'Строительство',
'retail': 'Розничная торговля',
'wholesale': 'Оптовая торговля',
'logistics': 'Логистика',
'healthcare': 'Здравоохранение',
'education': 'Образование',
'consulting': 'Консалтинг',
'marketing': 'Маркетинг',
'realestate': 'Недвижимость',
'food': 'Пищевая промышленность',
'agriculture': 'Сельское хозяйство',
'energy': 'Энергетика',
'telecom': 'Телекоммуникации',
'media': 'Медиа',
'tourism': 'Туризм',
'legal': 'Юридические услуги',
'other': 'Другое'
};
async function validateCompanies() {
try {
console.log('[Validation] Connecting to MongoDB...');
await mongoose.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
});
console.log('[Validation] Connected to MongoDB\n');
const allCompanies = await Company.find().exec();
console.log(`Found ${allCompanies.length} total companies\n`);
console.log('=== COMPANY DATA VALIDATION REPORT ===\n');
let issuesFound = 0;
let validCompanies = 0;
for (const company of allCompanies) {
console.log(`📋 Company: ${company.fullName}`);
console.log(` ID: ${company._id}`);
console.log(` Industry: ${company.industry} (type: ${typeof company.industry})`);
console.log(` Company Size: ${company.companySize}`);
let hasIssues = false;
if (company.industry) {
if (Array.isArray(company.industry)) {
console.log(` ⚠️ WARNING: industry is array!`);
issuesFound++;
hasIssues = true;
} else if (!Object.values(industryMap).includes(company.industry)) {
console.log(` ⚠️ industry value unknown: "${company.industry}"`);
issuesFound++;
hasIssues = true;
} else {
console.log(` ✅ industry OK`);
}
}
if (!hasIssues) validCompanies++;
console.log('');
}
console.log('\n=== SUMMARY ===');
console.log(`Total: ${allCompanies.length}`);
console.log(`Valid: ${validCompanies}`);
console.log(`Issues: ${issuesFound}`);
if (issuesFound === 0) {
console.log('\n✅ All data OK. No migration needed.');
} else {
console.log('\n⚠ Migration recommended.');
}
await mongoose.connection.close();
} catch (err) {
console.error('❌ Error:', err.message);
process.exit(1);
}
}
validateCompanies();