feature/worker #111
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": "Добавлено в избранное"
|
||||
}
|
||||
}
|
||||
@@ -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 сотрудников"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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-решения",
|
||||
"Услуги": "Профессиональные консалтинговые услуги",
|
||||
"Пищевая промышленность": "Качественная пищевая продукция",
|
||||
"Энергетика": "Энергоэффективные решения",
|
||||
"Медицина": "Современные медицинские технологии",
|
||||
"Образование": "Эффективные образовательные программы",
|
||||
"Финансы": "Надежные финансовые услуги",
|
||||
"Сельское хозяйство": "Современные агротехнологии"
|
||||
}
|
||||
}
|
||||
@@ -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}}"
|
||||
}
|
||||
}
|
||||
43
server/routers/procurement/models/BuyDocument.js
Normal file
43
server/routers/procurement/models/BuyDocument.js
Normal 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);
|
||||
|
||||
@@ -30,6 +30,7 @@ const buyProductSchema = new mongoose.Schema({
|
||||
url: String,
|
||||
type: String,
|
||||
size: Number,
|
||||
storagePath: String,
|
||||
uploadedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
|
||||
46
server/routers/procurement/models/Experience.js
Normal file
46
server/routers/procurement/models/Experience.js
Normal 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: 'ООО "ТеxПроект"',
|
||||
shortName: 'ООО "ТеxПроект"',
|
||||
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: 'ООО "ТеxПроект"',
|
||||
shortName: 'ООО "ТеxПроект"',
|
||||
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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
61
server/routers/procurement/scripts/test-logging.js
Normal file
61
server/routers/procurement/scripts/test-logging.js
Normal 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('');
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user