Сделан баланс, проверка чеков, начата система создания серверов

This commit is contained in:
Georgiy Syralev
2025-09-18 16:26:11 +03:00
parent 515d31ee9e
commit cce9e7b996
54 changed files with 1914 additions and 316 deletions

View File

@@ -2,6 +2,12 @@ import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import authRoutes from './modules/auth/auth.routes';
import ticketRoutes from './modules/ticket/ticket.routes';
import checkRoutes from './modules/check/check.routes';
import proxmoxRoutes from '../proxmox/proxmox.routes';
import tariffRoutes from './modules/tariff';
import osRoutes from './modules/os';
import serverRoutes from './modules/server';
dotenv.config();
@@ -30,7 +36,19 @@ app.get('/', (req, res) => {
});
});
// Статические файлы чеков
import path from 'path';
app.use('/uploads/checks', express.static(path.join(__dirname, '../uploads/checks')));
app.use('/api/auth', authRoutes);
app.use('/api/ticket', ticketRoutes);
app.use('/api/check', checkRoutes);
app.use('/api/proxmox', proxmoxRoutes);
app.use('/api/tariff', tariffRoutes);
app.use('/api/os', osRoutes);
app.use('/api/server', serverRoutes);
const PORT = process.env.PORT || 5000;

View File

@@ -67,11 +67,10 @@ export const login = async (req: Request, res: Response) => {
export const getMe = async (req: Request, res: Response) => {
try {
const userId = (req as any).userId;
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ message: 'Не авторизован.' });
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
@@ -79,18 +78,19 @@ export const getMe = async (req: Request, res: Response) => {
username: true,
email: true,
createdAt: true,
operator: true, // Добавляем поле operator
operator: true,
balance: true,
servers: true,
tickets: true,
},
});
console.log('API /api/auth/me user:', user);
if (!user) {
return res.status(404).json({ message: 'Пользователь не найден.' });
}
res.status(200).json({ user });
} catch (error) {
console.error('Ошибка при получении данных пользователя:', error);
res.status(500).json({ message: 'Ошибка сервера.' });
}
}
};

View File

@@ -1,13 +1,11 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
interface AuthRequest extends Request {
userId?: number;
}
const prisma = new PrismaClient();
const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_key';
export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
@@ -20,9 +18,10 @@ export const authMiddleware = (req: AuthRequest, res: Response, next: NextFuncti
}
const decoded = jwt.verify(token, JWT_SECRET) as { id: number };
req.userId = decoded.id;
const user = await prisma.user.findUnique({ where: { id: decoded.id } });
if (!user) return res.status(401).json({ message: 'Пользователь не найден.' });
req.user = user;
next();
} catch (error) {
console.error('Ошибка в мидлваре аутентификации:', error);
if (error instanceof jwt.JsonWebTokenError) {

View File

@@ -0,0 +1,66 @@
import { PrismaClient } from '@prisma/client';
import { Request, Response } from 'express';
import { Multer } from 'multer';
import path from 'path';
import fs from 'fs';
const prisma = new PrismaClient();
// Тип расширенного запроса с Multer
interface MulterRequest extends Request {
file?: Express.Multer.File;
}
// Загрузка чека клиентом (с файлом)
export async function uploadCheck(req: MulterRequest, res: Response) {
const userId = req.user?.id;
const { amount } = req.body;
const file = req.file;
if (!userId || !amount || !file) return res.status(400).json({ error: 'Данные не заполнены или файл не загружен' });
// Сохраняем путь к файлу
const fileUrl = `/uploads/checks/${file.filename}`;
const check = await prisma.check.create({
data: { userId, amount: Number(amount), fileUrl }
});
res.json(check);
}
// Получить все чеки (оператор)
export async function getChecks(req: Request, res: Response) {
const isOperator = Number(req.user?.operator) === 1;
if (!isOperator) return res.status(403).json({ error: 'Нет прав' });
const checks = await prisma.check.findMany({
include: { user: true },
orderBy: { createdAt: 'desc' }
});
res.json(checks);
}
// Подтвердить чек и пополнить баланс
export async function approveCheck(req: Request, res: Response) {
const { checkId } = req.body;
// Найти чек
const check = await prisma.check.findUnique({ where: { id: checkId } });
if (!check) return res.status(404).json({ error: 'Чек не найден' });
// Обновить статус
await prisma.check.update({ where: { id: checkId }, data: { status: 'approved' } });
// Пополнить баланс пользователя
await prisma.user.update({
where: { id: check.userId },
data: {
balance: {
increment: check.amount
}
}
});
res.json({ success: true });
}
// Отклонить чек
export async function rejectCheck(req: Request, res: Response) {
const { checkId } = req.body;
await prisma.check.update({ where: { id: checkId }, data: { status: 'rejected' } });
res.json({ success: true });
}

View File

@@ -0,0 +1,48 @@
import { Router } from 'express';
import { uploadCheck, getChecks, approveCheck, rejectCheck } from './check.controller';
import { authMiddleware } from '../auth/auth.middleware';
import multer, { MulterError } from 'multer';
import path from 'path';
const router = Router();
// Настройка Multer для загрузки чеков
const storage = multer.diskStorage({
destination: function (req: Express.Request, file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) {
cb(null, path.join(__dirname, '../../../uploads/checks'));
},
filename: function (req: Express.Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname);
}
});
const allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/jpg'
];
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
// Кастомная ошибка для Multer
const err: any = new Error('Недопустимый формат файла. Разрешены только изображения: jpg, jpeg, png, gif, webp.');
err.code = 'LIMIT_FILE_FORMAT';
cb(err, false);
}
}
});
router.use(authMiddleware);
router.post('/upload', upload.single('file'), uploadCheck);
router.get('/', getChecks);
router.post('/approve', approveCheck);
router.post('/reject', rejectCheck);
export default router;

View File

@@ -0,0 +1,2 @@
import osRoutes from './os.routes';
export default osRoutes;

View File

@@ -0,0 +1,18 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
const router = Router();
const prisma = new PrismaClient();
// GET /api/os — получить все ОС
router.get('/', async (req, res) => {
try {
const oses = await prisma.operatingSystem.findMany();
res.json(oses);
} catch (err) {
console.error('Ошибка получения ОС:', err);
res.status(500).json({ error: 'Ошибка получения ОС' });
}
});
export default router;

View File

@@ -0,0 +1,2 @@
import serverRoutes from './server.routes';
export default serverRoutes;

View File

@@ -0,0 +1,70 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
// import { createProxmoxContainer } from '../../proxmox/proxmoxApi'; // если есть интеграция
const router = Router();
const prisma = new PrismaClient();
// POST /api/server/create — создать сервер (контейнер)
router.post('/create', async (req, res) => {
try {
const { tariffId, osId } = req.body;
// TODO: получить userId из авторизации (req.user)
const userId = 1; // временно, заменить на реального пользователя
// Получаем тариф и ОС
const tariff = await prisma.tariff.findUnique({ where: { id: tariffId } });
const os = await prisma.operatingSystem.findUnique({ where: { id: osId } });
if (!tariff || !os) {
return res.status(400).json({ error: 'Тариф или ОС не найдены' });
}
// TODO: интеграция с Proxmox для создания контейнера
// Если интеграция с Proxmox есть, то только при успешном создании контейнера создавать запись в БД
// Например:
// let proxmoxResult;
// try {
// proxmoxResult = await createProxmoxContainer({ ... });
// } catch (proxmoxErr) {
// console.error('Ошибка Proxmox:', proxmoxErr);
// return res.status(500).json({ error: 'Ошибка создания контейнера на Proxmox' });
// }
// Если всё успешно — создаём запись сервера в БД
const server = await prisma.server.create({
data: {
userId,
tariffId,
osId,
status: 'creating',
},
});
res.json({ success: true, server });
} catch (err) {
console.error('Ошибка создания сервера:', err);
// Не создавать сервер, если есть ошибка
return res.status(500).json({ error: 'Ошибка создания сервера' });
}
});
// GET /api/server — получить все серверы пользователя
router.get('/', async (req, res) => {
try {
// TODO: получить userId из авторизации (req.user)
const userId = 1; // временно
const servers = await prisma.server.findMany({
where: { userId },
include: {
os: true,
tariff: true,
},
});
console.log('API /api/server ответ:', servers);
res.json(servers);
} catch (err) {
console.error('Ошибка получения серверов:', err);
res.status(500).json({ error: 'Ошибка получения серверов' });
}
});
export default router;

View File

@@ -0,0 +1,2 @@
import tariffRoutes from './tariff.routes';
export default tariffRoutes;

View File

@@ -0,0 +1,18 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
const router = Router();
const prisma = new PrismaClient();
// GET /api/tariff — получить все тарифы
router.get('/', async (req, res) => {
try {
const tariffs = await prisma.tariff.findMany();
res.json(tariffs);
} catch (err) {
console.error('Ошибка получения тарифов:', err);
res.status(500).json({ error: 'Ошибка получения тарифов' });
}
});
export default router;

View File

@@ -0,0 +1,93 @@
import { PrismaClient } from '@prisma/client';
import { Request, Response } from 'express';
const prisma = new PrismaClient();
// Расширяем тип Request для user
declare global {
namespace Express {
interface Request {
user?: {
id: number;
operator?: number;
// можно добавить другие поля при необходимости
};
}
}
}
// Создать тикет
export async function createTicket(req: Request, res: Response) {
const { title, message } = req.body;
const userId = req.user?.id;
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
try {
const ticket = await prisma.ticket.create({
data: { title, message, userId },
});
res.json(ticket);
} catch (err) {
res.status(500).json({ error: 'Ошибка создания тикета' });
}
}
// Получить тикеты (клиент — свои, оператор — все)
export async function getTickets(req: Request, res: Response) {
const userId = req.user?.id;
const isOperator = Number(req.user?.operator) === 1;
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
try {
const tickets = await prisma.ticket.findMany({
where: isOperator ? {} : { userId },
include: {
responses: { include: { operator: true } },
user: true
},
orderBy: { createdAt: 'desc' },
});
res.json(tickets);
} catch (err) {
res.status(500).json({ error: 'Ошибка получения тикетов' });
}
}
// Ответить на тикет (только оператор)
export async function respondTicket(req: Request, res: Response) {
const { ticketId, message } = req.body;
const operatorId = req.user?.id;
const isOperator = Number(req.user?.operator) === 1;
if (!operatorId || !isOperator) return res.status(403).json({ error: 'Нет прав' });
try {
const response = await prisma.response.create({
data: { ticketId, operatorId, message },
});
await prisma.ticket.update({
where: { id: ticketId },
data: { status: 'answered' },
});
res.json(response);
} catch (err) {
res.status(500).json({ error: 'Ошибка ответа на тикет' });
}
}
// Закрыть тикет (клиент или оператор)
export async function closeTicket(req: Request, res: Response) {
const { ticketId } = req.body;
const userId = req.user?.id;
const isOperator = Number(req.user?.operator) === 1;
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
try {
const ticket = await prisma.ticket.findUnique({ where: { id: ticketId } });
if (!ticket) return res.status(404).json({ error: 'Тикет не найден' });
if (!isOperator && ticket.userId !== userId) return res.status(403).json({ error: 'Нет прав' });
await prisma.ticket.update({
where: { id: ticketId },
data: { status: 'closed' },
});
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Ошибка закрытия тикета' });
}
}

View File

@@ -0,0 +1,14 @@
import { Router } from 'express';
import { createTicket, getTickets, respondTicket, closeTicket } from './ticket.controller';
import { authMiddleware } from '../auth/auth.middleware';
const router = Router();
router.use(authMiddleware);
router.post('/create', createTicket);
router.get('/', getTickets);
router.post('/respond', respondTicket);
router.post('/close', closeTicket);
export default router;