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

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

@@ -10,12 +10,14 @@
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.16.2",
"axios": "^1.12.2",
"bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2"
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
@@ -23,6 +25,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.23",
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^20.12.12",
"prisma": "^6.16.2",
"ts-node-dev": "^2.0.0",
@@ -296,10 +299,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "20.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.15.tgz",
"integrity": "sha512-W3bqcbLsRdFDVcmAM5l6oLlcl67vjevn8j1FPZ4nx+K5jNoWCh+FC/btxFoBPnvQlrHHDwfjp1kjIEDfwJ0Mog==",
"version": "20.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -410,6 +423,12 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -423,6 +442,23 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -521,9 +557,19 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -617,6 +663,18 @@
"consola": "^3.2.3"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -624,6 +682,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@@ -723,6 +796,15 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -870,6 +952,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -992,6 +1089,42 @@
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1155,6 +1288,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1478,7 +1626,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1503,6 +1650,36 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/multer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"mkdirp": "^0.5.6",
"object-assign": "^4.1.1",
"type-is": "^1.6.18",
"xtend": "^4.0.2"
},
"engines": {
"node": ">= 10.16.0"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1729,6 +1906,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@@ -1796,6 +1979,20 @@
"destr": "^2.0.3"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2045,6 +2242,23 @@
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -2260,6 +2474,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
@@ -2290,6 +2510,12 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -2326,7 +2552,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"

View File

@@ -13,12 +13,14 @@
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.16.2",
"axios": "^1.12.2",
"bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2"
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
@@ -26,6 +28,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.23",
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^20.12.12",
"prisma": "^6.16.2",
"ts-node-dev": "^2.0.0",

View File

@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.operatingSystem.deleteMany({ where: { type: 'windows' } });
console.log('Все Windows Server ОС удалены!');
}
main().finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,5 @@
-- AddForeignKey
ALTER TABLE `Ticket` ADD CONSTRAINT `Ticket_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Response` ADD CONSTRAINT `Response_operatorId_fkey` FOREIGN KEY (`operatorId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE `Check` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`amount` DOUBLE NOT NULL,
`status` VARCHAR(191) NOT NULL DEFAULT 'pending',
`fileUrl` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Check` ADD CONSTRAINT `Check_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `user` ADD COLUMN `balance` DOUBLE NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,45 @@
-- CreateTable
CREATE TABLE `Tariff` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`price` DOUBLE NOT NULL,
`description` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `Tariff_name_key`(`name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `OperatingSystem` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`type` VARCHAR(191) NOT NULL,
`template` VARCHAR(191) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `OperatingSystem_name_key`(`name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Server` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`tariffId` INTEGER NOT NULL,
`osId` INTEGER NOT NULL,
`status` VARCHAR(191) NOT NULL DEFAULT 'stopped',
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Server` ADD CONSTRAINT `Server_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Server` ADD CONSTRAINT `Server_tariffId_fkey` FOREIGN KEY (`tariffId`) REFERENCES `Tariff`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Server` ADD CONSTRAINT `Server_osId_fkey` FOREIGN KEY (`osId`) REFERENCES `OperatingSystem`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,4 +1,3 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
@@ -10,14 +9,61 @@ datasource db {
url = env("DATABASE_URL")
}
// This is your Prisma schema file,
model Tariff {
id Int @id @default(autoincrement())
name String @unique
price Float
description String?
createdAt DateTime @default(now())
servers Server[]
}
model OperatingSystem {
id Int @id @default(autoincrement())
name String @unique
type String // linux, windows, etc
template String? // путь к шаблону для контейнера
createdAt DateTime @default(now())
servers Server[]
}
model Server {
id Int @id @default(autoincrement())
userId Int
tariffId Int
osId Int
status String @default("stopped") // running, stopped, etc
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
tariff Tariff @relation(fields: [tariffId], references: [id])
os OperatingSystem @relation(fields: [osId], references: [id])
}
model User {
id Int @id @default(autoincrement())
username String
email String @unique
password String
createdAt DateTime @default(now())
plans Plan[]
operator Int @default(0) // Добавляем новую колонку operator
plans Plan[] @relation("UserPlans")
operator Int @default(0)
tickets Ticket[] @relation("UserTickets")
responses Response[] @relation("OperatorResponses")
checks Check[] @relation("UserChecks")
balance Float @default(0)
servers Server[]
}
model Check {
id Int @id @default(autoincrement())
userId Int
amount Float
status String @default("pending") // pending, approved, rejected
fileUrl String
createdAt DateTime @default(now())
user User @relation("UserChecks", fields: [userId], references: [id])
}
model Plan {
@@ -28,8 +74,8 @@ model Plan {
isCustom Boolean @default(false)
createdAt DateTime @default(now())
userId Int
owner User @relation(fields: [userId], references: [id])
services Service[]
owner User @relation("UserPlans", fields: [userId], references: [id])
services Service[] @relation("PlanServices")
}
model Service {
@@ -37,7 +83,7 @@ model Service {
name String @unique
price Float
planId Int?
plan Plan? @relation(fields: [planId], references: [id])
plan Plan? @relation("PlanServices", fields: [planId], references: [id])
}
model Ticket {
@@ -48,10 +94,8 @@ model Ticket {
status String @default("open")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
responses Response[] // связь
// Если нужна связь с User:
// user User @relation(fields: [userId], references: [id])
responses Response[] @relation("TicketResponses")
user User? @relation("UserTickets", fields: [userId], references: [id])
}
model Response {
@@ -60,5 +104,6 @@ model Response {
operatorId Int
message String
createdAt DateTime @default(now())
ticket Ticket @relation(fields: [ticketId], references: [id]) // <-- обратная связь
ticket Ticket @relation("TicketResponses", fields: [ticketId], references: [id])
operator User @relation("OperatorResponses", fields: [operatorId], references: [id])
}

View File

@@ -0,0 +1,27 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const tariffs = [
{ name: 'Минимальный', price: 150, description: '1 ядро, 1ГБ RAM, 20ГБ SSD' },
{ name: 'Базовый', price: 300, description: '2 ядра, 2ГБ RAM, 40ГБ SSD' },
{ name: 'Старт', price: 500, description: '2 ядра, 4ГБ RAM, 60ГБ SSD' },
{ name: 'Оптимальный', price: 700, description: '4 ядра, 4ГБ RAM, 80ГБ SSD' },
{ name: 'Профи', price: 1000, description: '4 ядра, 8ГБ RAM, 120ГБ SSD' },
{ name: 'Бизнес', price: 1500, description: '8 ядер, 16ГБ RAM, 200ГБ SSD' },
{ name: 'Корпоративный', price: 2000, description: '12 ядер, 24ГБ RAM, 300ГБ SSD' },
{ name: 'Премиум', price: 2500, description: '16 ядер, 32ГБ RAM, 400ГБ SSD' },
{ name: 'Энтерпрайз', price: 2800, description: '24 ядра, 48ГБ RAM, 500ГБ SSD' },
{ name: 'Максимум', price: 3000, description: '32 ядра, 64ГБ RAM, 1ТБ SSD' },
];
for (const t of tariffs) {
await prisma.tariff.upsert({
where: { name: t.name },
update: t,
create: t,
});
}
console.log('Тарифы успешно добавлены!');
}
main().finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,24 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const oses = [
{ name: 'Ubuntu 22.04', type: 'linux', template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst' },
{ name: 'Debian 12', type: 'linux', template: 'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst' },
{ name: 'CentOS 9', type: 'linux', template: 'local:vztmpl/centos-9-stream-default_20240828_amd64.tar.xz' },
{ name: 'AlmaLinux 9', type: 'linux', template: 'local:vztmpl/almalinux-9-default_20240911_amd64.tar.xz' },
{ name: 'Rocky Linux 9', type: 'linux', template: 'local:vztmpl/rockylinux-9-default_20240912_amd64.tar.xz' },
{ name: 'Arch Linux', type: 'linux', template: 'local:vztmpl/archlinux-base_20240911-1_amd64.tar.zst' },
{ name: 'Fedora 41', type: 'linux', template: 'local:vztmpl/fedora-41-default_20241118_amd64.tar.xz' },
];
for (const os of oses) {
await prisma.operatingSystem.upsert({
where: { name: os.name },
update: os,
create: os,
});
}
console.log('ОС успешно добавлены!');
}
main().finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,2 @@
// Импорт и экспорт функций для работы с Proxmox
export * from './proxmoxApi';

View File

@@ -0,0 +1,18 @@
import { Router } from 'express';
import { createContainer } from './proxmoxApi';
const router = Router();
// Маршрут для создания контейнера
router.post('/container', async (req, res) => {
try {
const { vmid, hostname, password, ostemplate, storage, cores, memory, rootfsSize } = req.body;
const result = await createContainer({ vmid, hostname, password, ostemplate, storage, cores, memory, rootfsSize });
res.json(result);
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : err });
}
});
export default router;

View File

@@ -0,0 +1,45 @@
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
const PROXMOX_API = `https://${process.env.PROXMOX_HOST}:${process.env.PROXMOX_PORT}/api2/json`;
function getProxmoxHeaders() {
return {
Authorization: `PVEAPIToken=${process.env.PROXMOX_API_TOKEN_ID}=${process.env.PROXMOX_API_TOKEN_SECRET}`,
};
}
// Создание контейнера (LXC) из шаблона
export interface CreateContainerParams {
vmid: number;
hostname: string;
password: string;
ostemplate: string;
storage: string;
cores: number;
memory: number;
rootfsSize: number;
}
export async function createContainer({ vmid, hostname, password, ostemplate, storage, cores, memory, rootfsSize }: CreateContainerParams) {
const url = `${PROXMOX_API}/nodes/${process.env.PROXMOX_NODE}/lxc`;
const data = {
vmid,
hostname,
password,
ostemplate, // например: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst'
storage, // например: 'local'
cores, // количество ядер
memory, // RAM в МБ
rootfs: `${storage}:${rootfsSize}`, // например: 'local:8'
net0: 'name=eth0,bridge=vmbr0,ip=dhcp',
// Дополнительные параметры по необходимости
};
try {
const res = await axios.post(url, data, { headers: getProxmoxHeaders() });
return res.data;
} catch (err) {
throw new Error('Ошибка создания контейнера: ' + (err instanceof Error ? err.message : err));
}
}

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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB