// server.js - с проверкой наличия env переменных const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const Minio = require('minio'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); // MongoDB Connection const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/'; mongoose.connect(mongoUri + 'procurement_db', { useNewUrlParser: true, useUnifiedTopology: true, }).then(() => { console.log('Connected to MongoDB'); }).catch(err => { console.error('MongoDB connection error:', err); }); // MinIO Client - с проверкой наличия переменных let minioClient = null; if (process.env.MINIO_ENDPOINT) { const endpoint = process.env.MINIO_ENDPOINT.replace('http://', '').replace('https://', ''); const [host, port] = endpoint.includes(':') ? endpoint.split(':') : [endpoint, '9000']; minioClient = new Minio.Client({ endPoint: host, port: parseInt(port), useSSL: false, accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin123', }); console.log('MinIO client configured'); } else { console.warn('MinIO not configured - file operations will not work'); } // JWT Secret const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production'; // Schemas const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true }, email: String, createdAt: { type: Date, default: Date.now } }); const procurementSchema = new mongoose.Schema({ number: String, name: String, organizer: String, status: String, end_date: String, price: String, link: String, details_loaded: Boolean, documents_links: [String], buyer: String, category: String, consumer: String, contact_person: String, email: String, phone: String, position: String, positions: [{ '№': String, 'Наименование, технические характеристики': String, 'Характеристики': String, 'Количество': String, 'Ед. изм.': String, 'Дата поставки': String, 'Комментарий организатора': String }], purchase_method: String, stars: { type: Number, default: 0 } }); const userProcurementSchema = new mongoose.Schema({ procurementId: { type: mongoose.Schema.Types.ObjectId, ref: 'Procurement' }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, status: { type: String, enum: ['participating', 'maybe', 'not_interested', null] }, comment: String, updatedAt: { type: Date, default: Date.now } }); const keywordSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, word: String, enabled: { type: Boolean, default: true }, createdAt: { type: Date, default: Date.now } }); const priceListSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, code: String, name: String, enabled: { type: Boolean, default: true }, createdAt: { type: Date, default: Date.now } }); const customRuleSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, condition: String, stars: { type: Number, min: 0, max: 5 }, enabled: { type: Boolean, default: true }, createdAt: { type: Date, default: Date.now } }); // Models const User = mongoose.model('User', userSchema); const Procurement = mongoose.model('Procurement', procurementSchema, 'procurements'); const UserProcurement = mongoose.model('UserProcurement', userProcurementSchema, 'userprocurements'); const Keyword = mongoose.model('Keyword', keywordSchema); const PriceList = mongoose.model('PriceList', priceListSchema); const CustomRule = mongoose.model('CustomRule', customRuleSchema); // Auth Middleware const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Access token required' }); } jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid token' }); req.user = user; next(); }); }; // Routes // Auth Routes app.post('/api/auth/login', async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ username }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } const isValid = await bcrypt.compare(password, user.password); if (!isValid) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign( { id: user._id, username: user.username }, JWT_SECRET, { expiresIn: '30d' } ); res.json({ token, user: { id: user._id, username: user.username } }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/auth/register', async (req, res) => { try { const { username, password, email } = req.body; // Проверяем, существует ли пользователь const existingUser = await User.findOne({ username }); if (existingUser) { return res.status(400).json({ error: 'Username already exists' }); } const hashedPassword = await bcrypt.hash(password, 10); const user = new User({ username, password: hashedPassword, email }); await user.save(); const token = jwt.sign( { id: user._id, username: user.username }, JWT_SECRET, { expiresIn: '30d' } ); res.json({ token, user: { id: user._id, username: user.username } }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Procurement Routes app.get('/api/procurements', authenticateToken, async (req, res) => { try { const { page = 1, limit = 20, search = '' } = req.query; const skip = (page - 1) * limit; const query = search ? { $or: [ { name: { $regex: search, $options: 'i' } }, { number: { $regex: search, $options: 'i' } }, { organizer: { $regex: search, $options: 'i' } } ] } : {}; const procurements = await Procurement.find(query) .skip(skip) .limit(parseInt(limit)); // Get user procurement statuses const userProcurements = await UserProcurement.find({ userId: req.user.id, procurementId: { $in: procurements.map(p => p._id) } }); const userProcMap = {}; userProcurements.forEach(up => { userProcMap[up.procurementId.toString()] = { status: up.status, comment: up.comment }; }); const result = procurements.map(p => ({ ...p.toObject(), userStatus: userProcMap[p._id.toString()] || null })); const total = await Procurement.countDocuments(query); res.json({ data: result, total, page: parseInt(page), totalPages: Math.ceil(total / limit) }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/procurements/:id', authenticateToken, async (req, res) => { try { const procurement = await Procurement.findById(req.params.id); if (!procurement) { return res.status(404).json({ error: 'Procurement not found' }); } const userProcurement = await UserProcurement.findOne({ userId: req.user.id, procurementId: procurement._id }); res.json({ ...procurement.toObject(), userStatus: userProcurement ? { status: userProcurement.status, comment: userProcurement.comment } : null }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/procurements/:id/status', authenticateToken, async (req, res) => { try { const { status, comment } = req.body; const userProcurement = await UserProcurement.findOneAndUpdate( { userId: req.user.id, procurementId: req.params.id }, { status, comment, updatedAt: new Date() }, { new: true, upsert: true } ); res.json(userProcurement); } catch (error) { res.status(500).json({ error: error.message }); } }); // File Proxy Route app.get('/api/files/*', authenticateToken, async (req, res) => { if (!minioClient) { return res.status(503).json({ error: 'File service not configured' }); } try { const filePath = req.params[0]; const bucket = process.env.MINIO_BUCKET || 'etp-files'; // Extract object name from the URL const objectName = filePath.replace(`${bucket}/`, ''); // Get object from MinIO minioClient.getObject(bucket, objectName, (err, dataStream) => { if (err) { return res.status(404).json({ error: 'File not found' }); } // Set appropriate headers res.setHeader('Content-Type', 'application/octet-stream'); // Pipe the file stream to response dataStream.pipe(res); }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Keywords Routes app.get('/api/keywords', authenticateToken, async (req, res) => { try { const keywords = await Keyword.find({ userId: req.user.id }); res.json(keywords); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/keywords', authenticateToken, async (req, res) => { try { const keyword = new Keyword({ userId: req.user.id, ...req.body }); await keyword.save(); res.json(keyword); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/keywords/:id', authenticateToken, async (req, res) => { try { const keyword = await Keyword.findOneAndUpdate( { _id: req.params.id, userId: req.user.id }, req.body, { new: true } ); res.json(keyword); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/keywords/:id', authenticateToken, async (req, res) => { try { await Keyword.findOneAndDelete({ _id: req.params.id, userId: req.user.id }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Price List Routes app.get('/api/pricelist', authenticateToken, async (req, res) => { try { const items = await PriceList.find({ userId: req.user.id }); res.json(items); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/pricelist', authenticateToken, async (req, res) => { try { const item = new PriceList({ userId: req.user.id, ...req.body }); await item.save(); res.json(item); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/pricelist/:id', authenticateToken, async (req, res) => { try { const item = await PriceList.findOneAndUpdate( { _id: req.params.id, userId: req.user.id }, req.body, { new: true } ); res.json(item); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/pricelist/:id', authenticateToken, async (req, res) => { try { await PriceList.findOneAndDelete({ _id: req.params.id, userId: req.user.id }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Custom Rules Routes app.get('/api/rules', authenticateToken, async (req, res) => { try { const rules = await CustomRule.find({ userId: req.user.id }); res.json(rules); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/rules', authenticateToken, async (req, res) => { try { const rule = new CustomRule({ userId: req.user.id, ...req.body }); await rule.save(); res.json(rule); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/rules/:id', authenticateToken, async (req, res) => { try { const rule = await CustomRule.findOneAndUpdate( { _id: req.params.id, userId: req.user.id }, req.body, { new: true } ); res.json(rule); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/rules/:id', authenticateToken, async (req, res) => { try { await CustomRule.findOneAndDelete({ _id: req.params.id, userId: req.user.id }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Start server app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`MongoDB: ${mongoUri}`); console.log(`MinIO configured: ${!!minioClient}`); });