замечания 3

This commit is contained in:
2025-11-02 12:40:42 +03:00
parent 35493a09b5
commit 0d1dcf21c1
29 changed files with 1498 additions and 1827 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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