Initial commit

This commit is contained in:
MUTS 2026-01-21 08:13:53 +04:00
commit 14eacfba5d
24 changed files with 9461 additions and 0 deletions

517
server.js Normal file
View file

@ -0,0 +1,517 @@
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)');
});