Initial commit
This commit is contained in:
commit
14eacfba5d
24 changed files with 9461 additions and 0 deletions
517
server.js
Normal file
517
server.js
Normal 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)');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue