FinnTrack/server.js
2026-01-21 08:13:53 +04:00

517 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)');
});