Сделан баланс, проверка чеков, начата система создания серверов
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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: 'Ошибка сервера.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
66
ospabhost/backend/src/modules/check/check.controller.ts
Normal file
66
ospabhost/backend/src/modules/check/check.controller.ts
Normal 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 });
|
||||
}
|
||||
48
ospabhost/backend/src/modules/check/check.routes.ts
Normal file
48
ospabhost/backend/src/modules/check/check.routes.ts
Normal 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;
|
||||
2
ospabhost/backend/src/modules/os/index.ts
Normal file
2
ospabhost/backend/src/modules/os/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import osRoutes from './os.routes';
|
||||
export default osRoutes;
|
||||
18
ospabhost/backend/src/modules/os/os.routes.ts
Normal file
18
ospabhost/backend/src/modules/os/os.routes.ts
Normal 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;
|
||||
2
ospabhost/backend/src/modules/server/index.ts
Normal file
2
ospabhost/backend/src/modules/server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import serverRoutes from './server.routes';
|
||||
export default serverRoutes;
|
||||
70
ospabhost/backend/src/modules/server/server.routes.ts
Normal file
70
ospabhost/backend/src/modules/server/server.routes.ts
Normal 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;
|
||||
2
ospabhost/backend/src/modules/tariff/index.ts
Normal file
2
ospabhost/backend/src/modules/tariff/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import tariffRoutes from './tariff.routes';
|
||||
export default tariffRoutes;
|
||||
18
ospabhost/backend/src/modules/tariff/tariff.routes.ts
Normal file
18
ospabhost/backend/src/modules/tariff/tariff.routes.ts
Normal 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;
|
||||
93
ospabhost/backend/src/modules/ticket/ticket.controller.ts
Normal file
93
ospabhost/backend/src/modules/ticket/ticket.controller.ts
Normal 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: 'Ошибка закрытия тикета' });
|
||||
}
|
||||
}
|
||||
14
ospabhost/backend/src/modules/ticket/ticket.routes.ts
Normal file
14
ospabhost/backend/src/modules/ticket/ticket.routes.ts
Normal 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;
|
||||
Reference in New Issue
Block a user