import express from 'express'; import cors from 'cors'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import dotenv from 'dotenv'; import pg from 'pg'; dotenv.config(); const app = express(); const PORT = Number(process.env.PORT) || 3001; const JWT_SECRET = process.env.JWT_SECRET; const DATABASE_URL = process.env.DATABASE_URL; const ADMIN_USERNAME = process.env.ADMIN_USERNAME; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; const ACTIVE_WINDOW_MINUTES = Number(process.env.ACTIVE_WINDOW_MINUTES) || 15; if (!JWT_SECRET) { throw new Error('JWT_SECRET is not set. Add it to .env'); } if (!DATABASE_URL) { throw new Error('DATABASE_URL is not set. Add it to .env'); } if (!ADMIN_USERNAME || !ADMIN_PASSWORD) { console.warn('ADMIN_USERNAME/ADMIN_PASSWORD are not set. Admin login will be disabled.'); } const { Pool } = pg; const pool = new Pool({ connectionString: DATABASE_URL }); // Middleware app.use(cors()); app.use(express.json()); const authRequired = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ success: false, message: 'Токен не предоставлен' }); } try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).json({ success: false, message: 'Недействительный токен' }); } }; const adminRequired = (req, res, next) => { if (req.user?.role !== 'admin') { return res.status(403).json({ success: false, message: 'Доступ запрещён' }); } return next(); }; // Register endpoint app.post('/api/register', async (req, res) => { try { const { firstName, lastName, username, password } = req.body; // Validation if (!firstName || !lastName || !username || !password) { return res.status(400).json({ success: false, message: 'Все поля обязательны для заполнения' }); } // Check if user already exists const existing = await pool.query('SELECT id FROM users WHERE username = $1', [username]); if (existing.rows.length > 0) { return res.status(400).json({ success: false, message: 'Пользователь с таким именем уже существует' }); } // Hash password const hashedPassword = await bcrypt.hash(password, 10); const insert = await pool.query( 'INSERT INTO users (first_name, last_name, username, password_hash) VALUES ($1, $2, $3, $4) RETURNING id, first_name, last_name, username', [firstName, lastName, username, hashedPassword] ); const newUser = insert.rows[0]; res.status(201).json({ success: true, message: 'Регистрация прошла успешно', user: { id: newUser.id, firstName: newUser.first_name, lastName: newUser.last_name, username: newUser.username } }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }); // Login endpoint app.post('/api/login', async (req, res) => { try { const { username, password } = req.body; // Validation if (!username || !password) { return res.status(400).json({ success: false, message: 'Имя пользователя и пароль обязательны' }); } // Admin login (credentials stored in .env) if (ADMIN_USERNAME && ADMIN_PASSWORD && username === ADMIN_USERNAME && password === ADMIN_PASSWORD) { const token = jwt.sign( { userId: '00000000-0000-0000-0000-000000000000', username, role: 'admin' }, JWT_SECRET, { expiresIn: '24h' } ); return res.json({ success: true, message: 'Вход выполнен успешно', token, user: { id: 'admin', firstName: 'Администратор', lastName: '', username, role: 'admin' } }); } // Find user const result = await pool.query( 'SELECT id, first_name, last_name, username, password_hash FROM users WHERE username = $1', [username] ); const user = result.rows[0]; if (!user) { return res.status(401).json({ success: false, message: 'Неверные учетные данные' }); } // Check password const isPasswordValid = await bcrypt.compare(password, user.password_hash); if (!isPasswordValid) { return res.status(401).json({ success: false, message: 'Неверные учетные данные' }); } // Generate JWT token const token = jwt.sign( { userId: user.id, username: user.username }, JWT_SECRET, { expiresIn: '24h' } ); await pool.query('UPDATE users SET last_login_at = NOW() WHERE id = $1', [user.id]); res.json({ success: true, message: 'Вход выполнен успешно', token, user: { id: user.id, firstName: user.first_name, lastName: user.last_name, username: user.username } }); } catch (error) { console.error('Login error:', error); res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); // Protected route example app.get('/api/profile', authRequired, async (req, res) => { try { const userId = req.user.userId; const result = await pool.query( 'SELECT id, first_name, last_name, username FROM users WHERE id = $1', [userId] ); const user = result.rows[0]; if (!user) { return res.status(404).json({ success: false, message: 'Пользователь не найден' }); } return res.json({ success: true, user: { id: user.id, firstName: user.first_name, lastName: user.last_name, username: user.username } }); } catch (error) { console.error('Profile error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); // Get all users (for testing) app.get('/api/users', async (req, res) => { try { const result = await pool.query( 'SELECT id, first_name, last_name, username, created_at FROM users ORDER BY created_at DESC' ); const usersWithoutPasswords = result.rows.map(user => ({ id: user.id, firstName: user.first_name, lastName: user.last_name, username: user.username, createdAt: user.created_at })); return res.json({ success: true, users: usersWithoutPasswords }); } catch (error) { console.error('Users list error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); // Incomes app.get('/api/incomes', authRequired, async (req, res) => { try { const userId = req.user.userId; const result = await pool.query( 'SELECT id, description, amount, category, date, created_at FROM incomes WHERE user_id = $1 ORDER BY date DESC, created_at DESC', [userId] ); return res.json({ success: true, incomes: result.rows }); } catch (error) { console.error('Get incomes error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); app.post('/api/incomes', authRequired, async (req, res) => { try { const userId = req.user.userId; const { description, amount, category, date } = req.body; if (!description || amount === undefined || amount === null || !category || !date) { return res.status(400).json({ success: false, message: 'Все поля обязательны для заполнения' }); } const parsedAmount = Number(amount); if (!Number.isFinite(parsedAmount) || parsedAmount < 0) { return res.status(400).json({ success: false, message: 'Некорректная сумма' }); } const insert = await pool.query( 'INSERT INTO incomes (user_id, description, amount, category, date) VALUES ($1, $2, $3, $4, $5) RETURNING id, description, amount, category, date, created_at', [userId, description, parsedAmount, category, date] ); return res.status(201).json({ success: true, income: insert.rows[0] }); } catch (error) { console.error('Create income error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); app.delete('/api/incomes/:id', authRequired, async (req, res) => { try { const userId = req.user.userId; const { id } = req.params; const result = await pool.query( 'DELETE FROM incomes WHERE id = $1 AND user_id = $2 RETURNING id', [id, userId] ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'Запись не найдена' }); } return res.json({ success: true }); } catch (error) { console.error('Delete income error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); // Expenses app.get('/api/expenses', authRequired, async (req, res) => { try { const userId = req.user.userId; const result = await pool.query( 'SELECT id, description, amount, category, date, created_at FROM expenses WHERE user_id = $1 ORDER BY date DESC, created_at DESC', [userId] ); return res.json({ success: true, expenses: result.rows }); } catch (error) { console.error('Get expenses error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); app.post('/api/expenses', authRequired, async (req, res) => { try { const userId = req.user.userId; const { description, amount, category, date } = req.body; if (!description || amount === undefined || amount === null || !category || !date) { return res.status(400).json({ success: false, message: 'Все поля обязательны для заполнения' }); } const parsedAmount = Number(amount); if (!Number.isFinite(parsedAmount) || parsedAmount < 0) { return res.status(400).json({ success: false, message: 'Некорректная сумма' }); } const insert = await pool.query( 'INSERT INTO expenses (user_id, description, amount, category, date) VALUES ($1, $2, $3, $4, $5) RETURNING id, description, amount, category, date, created_at', [userId, description, parsedAmount, category, date] ); return res.status(201).json({ success: true, expense: insert.rows[0] }); } catch (error) { console.error('Create expense error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); app.delete('/api/expenses/:id', authRequired, async (req, res) => { try { const userId = req.user.userId; const { id } = req.params; const result = await pool.query( 'DELETE FROM expenses WHERE id = $1 AND user_id = $2 RETURNING id', [id, userId] ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'Запись не найдена' }); } return res.json({ success: true }); } catch (error) { console.error('Delete expense error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); // Admin app.get('/api/admin/users', authRequired, adminRequired, async (req, res) => { try { const result = await pool.query( 'SELECT id, first_name, last_name, username, created_at, last_login_at FROM users ORDER BY created_at DESC' ); const usersList = result.rows.map(user => ({ id: user.id, firstName: user.first_name, lastName: user.last_name, username: user.username, createdAt: user.created_at, lastLoginAt: user.last_login_at })); return res.json({ success: true, users: usersList }); } catch (error) { console.error('Admin users list error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); app.get('/api/admin/active-users', authRequired, adminRequired, async (req, res) => { try { const result = await pool.query( 'SELECT id, first_name, last_name, username, last_login_at FROM users WHERE last_login_at IS NOT NULL AND last_login_at > (NOW() - ($1::int * INTERVAL \'1 minute\')) ORDER BY last_login_at DESC', [ACTIVE_WINDOW_MINUTES] ); const usersList = result.rows.map(user => ({ id: user.id, firstName: user.first_name, lastName: user.last_name, username: user.username, lastLoginAt: user.last_login_at })); return res.json({ success: true, activeWindowMinutes: ACTIVE_WINDOW_MINUTES, users: usersList }); } catch (error) { console.error('Admin active users error:', error); return res.status(500).json({ success: false, message: 'Внутренняя ошибка сервера' }); } }); // Start server app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log('Available endpoints:'); console.log('POST /api/register - Register new user'); console.log('POST /api/login - Login user'); console.log('GET /api/profile - Get user profile (protected)'); console.log('GET /api/users - Get all users (for testing)'); });