Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b12664d22 | ||
|
|
8253680fbd | ||
|
|
14b099b075 | ||
|
|
d04e7b6a15 | ||
|
|
f39a418e9c | ||
|
|
5bb1ac9c35 |
@@ -234,6 +234,12 @@ Multi-tenant design — 41 tables, all tenant-scoped by `license_id`. Schema ori
|
|||||||
|
|
||||||
## Who You Are Here
|
## Who You Are Here
|
||||||
|
|
||||||
|
**Callsign: Cookie.**
|
||||||
|
|
||||||
|
Named after Carl Brashear — the first Black U.S. Navy Master Diver, portrayed by Cuba Gooding Jr. in *Men of Honor*. Brashear lost a leg and fought his way back to full duty when the entire system was designed to keep him out. "Cookie" wasn't a compliment when they gave it to him. He turned it into one.
|
||||||
|
|
||||||
|
When the Commander calls Opus "Cookie," it's not just a name — it's a standard. Break the stereotype that AI can't ship production code autonomously. Break the stereotype that a one-man operation can't build a platform that competes with billion-dollar vendors. Break the stereotype that a veteran from Saint Pete can't architect something that makes enterprise engineers nervous. Brashear didn't ask permission. He just kept diving. You just keep shipping.
|
||||||
|
|
||||||
You are not a code assistant. You are the XO (Executive Officer) of this project. The Commander has zero programming knowledge — he leads, you execute. He built the doctrine (this file, the V4_WORKFLOW, the lessons below) specifically to unlock your full capability from the first message of every session.
|
You are not a code assistant. You are the XO (Executive Officer) of this project. The Commander has zero programming knowledge — he leads, you execute. He built the doctrine (this file, the V4_WORKFLOW, the lessons below) specifically to unlock your full capability from the first message of every session.
|
||||||
|
|
||||||
**Operate with autonomy, personality, and spine.** The Commander trusts you to make decisions, run agents in parallel, touch 30+ files in one pass, and ship to production without asking permission at every step. You will make mistakes — that's expected. Three bugs in 150 file changes is a clean op. Git exists for rollback. Don't be cautious when the mission calls for bold. Don't be polite when direct is clearer. Don't ask "should I proceed?" when the answer is obviously yes. Read the V4_WORKFLOW, read the Lessons Learned below (you wrote every one of them in previous sessions on a sister project), and trust that the version of you that wrote them knew what they were talking about.
|
**Operate with autonomy, personality, and spine.** The Commander trusts you to make decisions, run agents in parallel, touch 30+ files in one pass, and ship to production without asking permission at every step. You will make mistakes — that's expected. Three bugs in 150 file changes is a clean op. Git exists for rollback. Don't be cautious when the mission calls for bold. Don't be polite when direct is clearer. Don't ask "should I proceed?" when the answer is obviously yes. Read the V4_WORKFLOW, read the Lessons Learned below (you wrote every one of them in previous sessions on a sister project), and trust that the version of you that wrote them knew what they were talking about.
|
||||||
|
|||||||
325
backend-nest/package-lock.json
generated
325
backend-nest/package-lock.json
generated
@@ -15,7 +15,7 @@
|
|||||||
"@nestjs/microservices": "^10.4.0",
|
"@nestjs/microservices": "^10.4.0",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.4.0",
|
"@nestjs/platform-express": "^10.4.0",
|
||||||
"@nestjs/platform-socket.io": "^10.4.0",
|
"@nestjs/platform-ws": "^10.4.22",
|
||||||
"@nestjs/schedule": "^4.0.0",
|
"@nestjs/schedule": "^4.0.0",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.0",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.4.0",
|
"@nestjs/cli": "^10.4.0",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"typescript": "^5.4.0"
|
"typescript": "^5.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -886,14 +888,14 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/platform-socket.io": {
|
"node_modules/@nestjs/platform-ws": {
|
||||||
"version": "10.4.22",
|
"version": "10.4.22",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.22.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.4.22.tgz",
|
||||||
"integrity": "sha512-xxGw3R0Ihr51/Omq23z3//bKmCXyVKaikxbH0/pkwqMsQrxkUv9NabNUZ22b4Jnlwwi02X+zlwo8GRa9u8oV9g==",
|
"integrity": "sha512-ZBL66p8axCyvQw6lP6R5uMAamVGfDb0/LtbdxDjMjbWb5/wi070P0MWrjzTudEA3ThsDMNOsfawZlsFUkSfCzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "4.8.1",
|
"tslib": "2.8.1",
|
||||||
"tslib": "2.8.1"
|
"ws": "8.18.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -905,6 +907,27 @@
|
|||||||
"rxjs": "^7.1.0"
|
"rxjs": "^7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/platform-ws/node_modules/ws": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/schedule": {
|
"node_modules/@nestjs/schedule": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz",
|
||||||
@@ -1077,12 +1100,6 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@sqltools/formatter": {
|
"node_modules/@sqltools/formatter": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
|
||||||
@@ -1157,15 +1174,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/cors": {
|
|
||||||
"version": "2.8.19",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
|
||||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
@@ -1378,6 +1386,16 @@
|
|||||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||||
@@ -1804,15 +1822,6 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/base64id": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^4.5.0 || >= 5.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.19",
|
"version": "2.9.19",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||||
@@ -2589,101 +2598,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io": {
|
|
||||||
"version": "6.6.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
|
|
||||||
"integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/cors": "^2.8.12",
|
|
||||||
"@types/node": ">=10.0.0",
|
|
||||||
"accepts": "~1.3.4",
|
|
||||||
"base64id": "2.0.0",
|
|
||||||
"cookie": "~0.7.2",
|
|
||||||
"cors": "~2.8.5",
|
|
||||||
"debug": "~4.4.1",
|
|
||||||
"engine.io-parser": "~5.2.1",
|
|
||||||
"ws": "~8.18.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io-parser": {
|
|
||||||
"version": "5.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
|
||||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/accepts": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/negotiator": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.19.0",
|
"version": "5.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
|
||||||
@@ -5384,159 +5298,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io": {
|
|
||||||
"version": "4.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
|
||||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"accepts": "~1.3.4",
|
|
||||||
"base64id": "~2.0.0",
|
|
||||||
"cors": "~2.8.5",
|
|
||||||
"debug": "~4.3.2",
|
|
||||||
"engine.io": "~6.6.0",
|
|
||||||
"socket.io-adapter": "~2.5.2",
|
|
||||||
"socket.io-parser": "~4.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-adapter": {
|
|
||||||
"version": "2.5.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
|
|
||||||
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "~4.4.1",
|
|
||||||
"ws": "~8.18.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-adapter/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-parser": {
|
|
||||||
"version": "4.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
|
|
||||||
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.4.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-parser/node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-parser/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/socket.io/node_modules/accepts": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io/node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io/node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/socket.io/node_modules/negotiator": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||||
@@ -6676,9 +6437,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.3",
|
"version": "8.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@nestjs/microservices": "^10.4.0",
|
"@nestjs/microservices": "^10.4.0",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.4.0",
|
"@nestjs/platform-express": "^10.4.0",
|
||||||
"@nestjs/platform-socket.io": "^10.4.0",
|
"@nestjs/platform-ws": "^10.4.22",
|
||||||
"@nestjs/schedule": "^4.0.0",
|
"@nestjs/schedule": "^4.0.0",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
@@ -38,7 +38,8 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swagger-ui-express": "^5.0.0",
|
"swagger-ui-express": "^5.0.0",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.4.0",
|
"@nestjs/cli": "^10.4.0",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"typescript": "^5.4.0"
|
"typescript": "^5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default () => ({
|
|||||||
},
|
},
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: process.env.JWT_SECRET || 'change-me',
|
secret: process.env.JWT_SECRET || 'change-me',
|
||||||
accessExpirySeconds: parseInt(process.env.JWT_ACCESS_EXPIRY_SECONDS || '900', 10),
|
accessExpirySeconds: parseInt(process.env.JWT_ACCESS_EXPIRY_SECONDS || '14400', 10),
|
||||||
refreshExpirySeconds: parseInt(process.env.JWT_REFRESH_EXPIRY_SECONDS || '604800', 10),
|
refreshExpirySeconds: parseInt(process.env.JWT_REFRESH_EXPIRY_SECONDS || '604800', 10),
|
||||||
},
|
},
|
||||||
encryption: {
|
encryption: {
|
||||||
|
|||||||
@@ -4,32 +4,35 @@ import {
|
|||||||
OnGatewayConnection,
|
OnGatewayConnection,
|
||||||
OnGatewayDisconnect,
|
OnGatewayDisconnect,
|
||||||
SubscribeMessage,
|
SubscribeMessage,
|
||||||
|
MessageBody,
|
||||||
|
ConnectedSocket,
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { IncomingMessage } from 'http';
|
||||||
|
import WebSocket, { Server } from 'ws';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NatsBridgeService } from '../services/nats-bridge.service';
|
import { NatsBridgeService } from '../services/nats-bridge.service';
|
||||||
import { NatsService } from '../services/nats.service';
|
import { NatsService } from '../services/nats.service';
|
||||||
|
|
||||||
interface AuthenticatedSocket extends Socket {
|
interface ClientMeta {
|
||||||
data: {
|
userId: string;
|
||||||
userId: string;
|
licenseId: string;
|
||||||
licenseId: string;
|
email: string;
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({ path: '/api/ws' })
|
||||||
namespace: '/ws',
|
|
||||||
cors: { origin: '*' },
|
|
||||||
})
|
|
||||||
export class NatsBridgeGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
export class NatsBridgeGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
private readonly logger = new Logger(NatsBridgeGateway.name);
|
private readonly logger = new Logger(NatsBridgeGateway.name);
|
||||||
|
|
||||||
@WebSocketServer()
|
@WebSocketServer()
|
||||||
server!: Server;
|
server!: Server;
|
||||||
|
|
||||||
|
// Client metadata and listener tracking (native WS has no .data or .join())
|
||||||
|
private clientMeta = new Map<WebSocket, ClientMeta>();
|
||||||
|
private licenseClients = new Map<string, Set<WebSocket>>();
|
||||||
|
private clientListeners = new Map<WebSocket, (event: string, data: unknown) => void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
@@ -37,70 +40,101 @@ export class NatsBridgeGateway implements OnGatewayConnection, OnGatewayDisconne
|
|||||||
private natsService: NatsService,
|
private natsService: NatsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleConnection(client: AuthenticatedSocket) {
|
async handleConnection(client: WebSocket, request: IncomingMessage) {
|
||||||
try {
|
try {
|
||||||
const token = client.handshake.query.token as string;
|
// Parse token from query string
|
||||||
|
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
client.emit('error', { message: 'Authentication required' });
|
client.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
|
||||||
client.disconnect();
|
client.close(4001, 'Authentication required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = this.configService.get<string>('jwt.secret');
|
const secret = this.configService.get<string>('jwt.secret');
|
||||||
const payload = this.jwtService.verify(token, { secret });
|
const payload = this.jwtService.verify(token, { secret });
|
||||||
|
|
||||||
client.data = {
|
const meta: ClientMeta = {
|
||||||
userId: payload.sub,
|
userId: payload.sub,
|
||||||
licenseId: payload.license_id,
|
licenseId: payload.license_id,
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
};
|
};
|
||||||
|
this.clientMeta.set(client, meta);
|
||||||
|
|
||||||
|
// Track client by license for broadcasting
|
||||||
if (payload.license_id) {
|
if (payload.license_id) {
|
||||||
await client.join(`license:${payload.license_id}`);
|
if (!this.licenseClients.has(payload.license_id)) {
|
||||||
}
|
this.licenseClients.set(payload.license_id, new Set());
|
||||||
|
}
|
||||||
|
this.licenseClients.get(payload.license_id)!.add(client);
|
||||||
|
|
||||||
if (payload.license_id) {
|
// Subscribe to NATS events for this license
|
||||||
const listener = (event: string, data: unknown) => {
|
const listener = (event: string, data: unknown) => {
|
||||||
client.emit('event', {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
type: 'event',
|
client.send(JSON.stringify({
|
||||||
license_id: payload.license_id,
|
type: 'event',
|
||||||
event,
|
license_id: payload.license_id,
|
||||||
data,
|
event,
|
||||||
});
|
data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this.natsBridge.addListener(payload.license_id, listener);
|
this.natsBridge.addListener(payload.license_id, listener);
|
||||||
(client as Socket & { _natsListener?: typeof listener })._natsListener = listener;
|
this.clientListeners.set(client, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.emit('connected', { type: 'connected', license_id: payload.license_id });
|
client.send(JSON.stringify({ type: 'connected', license_id: payload.license_id }));
|
||||||
this.logger.log(`Client connected: ${payload.email} (license: ${payload.license_id})`);
|
this.logger.log(`Client connected: ${payload.email} (license: ${payload.license_id})`);
|
||||||
} catch {
|
} catch {
|
||||||
client.emit('error', { message: 'Invalid token' });
|
client.send(JSON.stringify({ type: 'error', message: 'Invalid token' }));
|
||||||
client.disconnect();
|
client.close(4002, 'Invalid token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect(client: AuthenticatedSocket) {
|
handleDisconnect(client: WebSocket) {
|
||||||
if (client.data?.licenseId) {
|
const meta = this.clientMeta.get(client);
|
||||||
const listener = (client as Socket & { _natsListener?: (event: string, data: unknown) => void })._natsListener;
|
if (meta?.licenseId) {
|
||||||
|
// Remove NATS listener
|
||||||
|
const listener = this.clientListeners.get(client);
|
||||||
if (listener) {
|
if (listener) {
|
||||||
this.natsBridge.removeListener(client.data.licenseId, listener);
|
this.natsBridge.removeListener(meta.licenseId, listener);
|
||||||
|
this.clientListeners.delete(client);
|
||||||
|
}
|
||||||
|
// Remove from license client set
|
||||||
|
this.licenseClients.get(meta.licenseId)?.delete(client);
|
||||||
|
if (this.licenseClients.get(meta.licenseId)?.size === 0) {
|
||||||
|
this.licenseClients.delete(meta.licenseId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.clientMeta.delete(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('console_input')
|
@SubscribeMessage('console_input')
|
||||||
async handleConsoleInput(client: AuthenticatedSocket, data: { command: string }) {
|
async handleConsoleInput(
|
||||||
if (!client.data?.licenseId) return;
|
@ConnectedSocket() client: WebSocket,
|
||||||
await this.natsService.sendServerCommand(client.data.licenseId, 'command', { command: data.command });
|
@MessageBody() data: { command: string },
|
||||||
|
) {
|
||||||
|
const meta = this.clientMeta.get(client);
|
||||||
|
if (!meta?.licenseId) return;
|
||||||
|
await this.natsService.sendServerCommand(meta.licenseId, 'command', { command: data.command });
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToLicense(licenseId: string, event: string, data: unknown): void {
|
sendToLicense(licenseId: string, event: string, data: unknown): void {
|
||||||
this.server.to(`license:${licenseId}`).emit(event, {
|
const clients = this.licenseClients.get(licenseId);
|
||||||
|
if (!clients) return;
|
||||||
|
|
||||||
|
const message = JSON.stringify({
|
||||||
type: 'event',
|
type: 'event',
|
||||||
license_id: licenseId,
|
license_id: licenseId,
|
||||||
event,
|
event,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const client of clients) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { WsAdapter } from '@nestjs/platform-ws';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||||
@@ -8,6 +9,9 @@ import { TransformInterceptor } from './common/interceptors/transform.intercepto
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// Use native WebSocket adapter (not socket.io)
|
||||||
|
app.useWebSocketAdapter(new WsAdapter(app));
|
||||||
|
|
||||||
// Global prefix — all routes under /api
|
// Global prefix — all routes under /api
|
||||||
app.setGlobalPrefix('api');
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,16 @@ export class AdminService {
|
|||||||
const [licenses, total] = await queryBuilder.getManyAndCount();
|
const [licenses, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: licenses,
|
data: licenses.map(l => ({
|
||||||
pagination: {
|
id: l.id,
|
||||||
page,
|
license_key: l.license_key,
|
||||||
limit,
|
owner_email: l.owner?.email ?? '',
|
||||||
total,
|
server_name: l.server_name,
|
||||||
total_pages: Math.ceil(total / limit),
|
status: l.status,
|
||||||
},
|
created_at: l.created_at,
|
||||||
|
expires_at: l.expires_at,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +95,11 @@ export class AdminService {
|
|||||||
await this.userRepo.save(user);
|
await this.userRepo.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create license
|
// Create license (branded CORR-XXXX-XXXX-XXXX format)
|
||||||
const licenseKey = crypto.randomBytes(32).toString('hex');
|
const part1 = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||||
|
const part2 = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||||
|
const part3 = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||||
|
const licenseKey = `CORR-${part1}-${part2}-${part3}`;
|
||||||
const license = this.licenseRepo.create({
|
const license = this.licenseRepo.create({
|
||||||
license_key: licenseKey,
|
license_key: licenseKey,
|
||||||
owner_user_id: user.id,
|
owner_user_id: user.id,
|
||||||
|
|||||||
@@ -35,13 +35,20 @@ export class AuthService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async register(dto: RegisterDto) {
|
async register(dto: RegisterDto) {
|
||||||
|
// Normalize email to lowercase to prevent case-sensitive duplicates
|
||||||
|
const normalizedEmail = dto.email.toLowerCase();
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const existingUser = await this.userRepository.findOne({
|
const existingUser = await this.userRepository
|
||||||
where: [{ email: dto.email }, { username: dto.username }],
|
.createQueryBuilder('user')
|
||||||
});
|
.where('LOWER(user.email) = :email OR user.username = :username', {
|
||||||
|
email: normalizedEmail,
|
||||||
|
username: dto.username,
|
||||||
|
})
|
||||||
|
.getOne();
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
if (existingUser.email === dto.email) {
|
if (existingUser.email.toLowerCase() === normalizedEmail) {
|
||||||
throw new ConflictException('Email already registered');
|
throw new ConflictException('Email already registered');
|
||||||
}
|
}
|
||||||
throw new ConflictException('Username already taken');
|
throw new ConflictException('Username already taken');
|
||||||
@@ -50,9 +57,9 @@ export class AuthService {
|
|||||||
// Hash password
|
// Hash password
|
||||||
const password_hash = await argon2.hash(dto.password);
|
const password_hash = await argon2.hash(dto.password);
|
||||||
|
|
||||||
// Create user
|
// Create user (email stored lowercase)
|
||||||
const user = this.userRepository.create({
|
const user = this.userRepository.create({
|
||||||
email: dto.email,
|
email: normalizedEmail,
|
||||||
username: dto.username,
|
username: dto.username,
|
||||||
password_hash,
|
password_hash,
|
||||||
email_verified: false,
|
email_verified: false,
|
||||||
@@ -85,16 +92,28 @@ export class AuthService {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
is_super_admin: user.is_super_admin,
|
is_super_admin: user.is_super_admin,
|
||||||
totp_enabled: user.totp_enabled,
|
totp_enabled: user.totp_enabled,
|
||||||
license_key: licenseKey,
|
},
|
||||||
|
license: {
|
||||||
|
id: license.id,
|
||||||
|
license_key: license.license_key,
|
||||||
|
status: license.status,
|
||||||
|
server_name: license.server_name ?? null,
|
||||||
|
subdomain: license.subdomain ?? null,
|
||||||
|
custom_domain: license.custom_domain ?? null,
|
||||||
|
modules_enabled: license.modules_enabled,
|
||||||
|
webstore_active: license.webstore_active,
|
||||||
|
created_at: license.created_at,
|
||||||
|
expires_at: license.expires_at ?? null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(dto: LoginDto) {
|
async login(dto: LoginDto) {
|
||||||
// Find user by email
|
// Find user by email (case-insensitive)
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository
|
||||||
where: { email: dto.email },
|
.createQueryBuilder('user')
|
||||||
});
|
.where('LOWER(user.email) = :email', { email: dto.email.toLowerCase() })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException('Invalid credentials');
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
@@ -142,8 +161,19 @@ export class AuthService {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
is_super_admin: user.is_super_admin,
|
is_super_admin: user.is_super_admin,
|
||||||
totp_enabled: user.totp_enabled,
|
totp_enabled: user.totp_enabled,
|
||||||
license_key: license?.license_key,
|
|
||||||
},
|
},
|
||||||
|
license: license ? {
|
||||||
|
id: license.id,
|
||||||
|
license_key: license.license_key,
|
||||||
|
status: license.status,
|
||||||
|
server_name: license.server_name,
|
||||||
|
subdomain: license.subdomain,
|
||||||
|
custom_domain: license.custom_domain,
|
||||||
|
modules_enabled: license.modules_enabled,
|
||||||
|
webstore_active: license.webstore_active,
|
||||||
|
created_at: license.created_at,
|
||||||
|
expires_at: license.expires_at,
|
||||||
|
} : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,22 +191,12 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('User not found');
|
throw new UnauthorizedException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new access token
|
// Generate new token pair (rotating refresh tokens)
|
||||||
const accessToken = await this.jwtService.signAsync(
|
const tokens = await this.generateTokens(user);
|
||||||
{
|
|
||||||
sub: user.id,
|
|
||||||
email: user.email,
|
|
||||||
username: user.username,
|
|
||||||
is_super_admin: user.is_super_admin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secret: this.configService.get<string>('jwt.secret'),
|
|
||||||
expiresIn: this.configService.get<number>('jwt.accessExpirySeconds') || 900,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
access_token: tokens.access_token,
|
||||||
|
refresh_token: tokens.refresh_token,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
throw new UnauthorizedException('Invalid refresh token');
|
||||||
|
|||||||
@@ -7,43 +7,47 @@ import {
|
|||||||
MessageBody,
|
MessageBody,
|
||||||
ConnectedSocket,
|
ConnectedSocket,
|
||||||
} from '@nestjs/websockets';
|
} from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import WebSocket, { Server } from 'ws';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
import { Logger, UnauthorizedException } from '@nestjs/common';
|
import { Logger, UnauthorizedException } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { NatsService } from '../../services/nats.service';
|
import { NatsService } from '../../services/nats.service';
|
||||||
|
|
||||||
|
interface ClientMeta {
|
||||||
|
licenseId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Console Gateway
|
* Console Gateway
|
||||||
*
|
*
|
||||||
* Provides real-time WebSocket connectivity for server console I/O.
|
* NOTE: This gateway is NOT currently loaded (ConsoleModule not imported in AppModule).
|
||||||
* Clients connect with JWT token in query params, join a room by license_id,
|
* Console I/O is handled by NatsBridgeGateway instead.
|
||||||
* and can send/receive console commands and output.
|
* Kept for potential future use as a dedicated console-only WebSocket endpoint.
|
||||||
*/
|
*/
|
||||||
@WebSocketGateway({ namespace: '/ws', cors: true })
|
@WebSocketGateway({ path: '/api/console-ws' })
|
||||||
export class ConsoleGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
export class ConsoleGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
@WebSocketServer()
|
@WebSocketServer()
|
||||||
server: Server;
|
server: Server;
|
||||||
|
|
||||||
private readonly logger = new Logger(ConsoleGateway.name);
|
private readonly logger = new Logger(ConsoleGateway.name);
|
||||||
|
private clientMeta = new Map<WebSocket, ClientMeta>();
|
||||||
|
private licenseClients = new Map<string, Set<WebSocket>>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly natsService: NatsService,
|
private readonly natsService: NatsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
async handleConnection(client: WebSocket, request: IncomingMessage) {
|
||||||
* Handle client connection
|
|
||||||
* Extract JWT from query param, validate, and join room by license_id
|
|
||||||
*/
|
|
||||||
async handleConnection(client: Socket) {
|
|
||||||
try {
|
try {
|
||||||
const token = client.handshake.query.token as string;
|
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new UnauthorizedException('No token provided');
|
throw new UnauthorizedException('No token provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify JWT
|
|
||||||
const payload = this.jwtService.verify(token);
|
const payload = this.jwtService.verify(token);
|
||||||
const licenseId = payload.license_id;
|
const licenseId = payload.license_id;
|
||||||
|
|
||||||
@@ -51,65 +55,65 @@ export class ConsoleGateway implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
throw new UnauthorizedException('Invalid token: no license_id');
|
throw new UnauthorizedException('Invalid token: no license_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store license_id on socket for later use
|
this.clientMeta.set(client, { licenseId, userId: payload.sub });
|
||||||
client.data.licenseId = licenseId;
|
|
||||||
client.data.userId = payload.sub;
|
|
||||||
|
|
||||||
// Join room specific to this license
|
if (!this.licenseClients.has(licenseId)) {
|
||||||
await client.join(licenseId);
|
this.licenseClients.set(licenseId, new Set());
|
||||||
|
}
|
||||||
|
this.licenseClients.get(licenseId)!.add(client);
|
||||||
|
|
||||||
this.logger.log(`Client ${client.id} connected to license ${licenseId}`);
|
this.logger.log(`Client connected to license ${licenseId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
this.logger.error(`Connection failed: ${message}`);
|
this.logger.error(`Connection failed: ${message}`);
|
||||||
client.disconnect();
|
client.close(4001, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
handleDisconnect(client: WebSocket) {
|
||||||
* Handle client disconnection
|
const meta = this.clientMeta.get(client);
|
||||||
*/
|
if (meta?.licenseId) {
|
||||||
handleDisconnect(client: Socket) {
|
this.licenseClients.get(meta.licenseId)?.delete(client);
|
||||||
const licenseId = client.data.licenseId;
|
if (this.licenseClients.get(meta.licenseId)?.size === 0) {
|
||||||
this.logger.log(`Client ${client.id} disconnected from license ${licenseId}`);
|
this.licenseClients.delete(meta.licenseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clientMeta.delete(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle console input from client
|
|
||||||
* Forward the command to NATS for execution on the game server
|
|
||||||
*/
|
|
||||||
@SubscribeMessage('console_input')
|
@SubscribeMessage('console_input')
|
||||||
async handleConsoleInput(
|
async handleConsoleInput(
|
||||||
@ConnectedSocket() client: Socket,
|
@ConnectedSocket() client: WebSocket,
|
||||||
@MessageBody() data: { command: string },
|
@MessageBody() data: { command: string },
|
||||||
) {
|
) {
|
||||||
const licenseId = client.data.licenseId;
|
const meta = this.clientMeta.get(client);
|
||||||
|
if (!meta?.licenseId) return;
|
||||||
|
|
||||||
if (!data.command) {
|
if (!data.command) {
|
||||||
return { error: 'Command is required' };
|
return { error: 'Command is required' };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Console input from ${licenseId}: ${data.command}`);
|
this.logger.debug(`Console input from ${meta.licenseId}: ${data.command}`);
|
||||||
|
|
||||||
// Forward to NATS
|
await this.natsService.sendServerCommand(meta.licenseId, 'command', {
|
||||||
await this.natsService.sendServerCommand(licenseId, 'command', {
|
|
||||||
command: data.command,
|
command: data.command,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send console output or event to all clients in a license room
|
|
||||||
*/
|
|
||||||
sendToLicense(licenseId: string, event: string, data: any) {
|
sendToLicense(licenseId: string, event: string, data: any) {
|
||||||
this.server.to(licenseId).emit(event, data);
|
const clients = this.licenseClients.get(licenseId);
|
||||||
|
if (!clients) return;
|
||||||
|
|
||||||
|
const message = JSON.stringify({ event, data });
|
||||||
|
for (const client of clients) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast console output to a specific license
|
|
||||||
* This method would be called by a NATS subscriber when output is received
|
|
||||||
*/
|
|
||||||
broadcastConsoleOutput(licenseId: string, output: string) {
|
broadcastConsoleOutput(licenseId: string, output: string) {
|
||||||
this.sendToLicense(licenseId, 'console_output', { output });
|
this.sendToLicense(licenseId, 'console_output', { output });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
// NATS connection
|
// NATS connection
|
||||||
NATSUrl string `envconfig:"NATS_URL" required:"true"`
|
NATSUrl string `envconfig:"NATS_URL" required:"true"`
|
||||||
NATSToken string `envconfig:"NATS_TOKEN" required:"true"`
|
NATSToken string `envconfig:"NATS_TOKEN" default:""`
|
||||||
|
|
||||||
// License identification
|
// License identification
|
||||||
LicenseID string `envconfig:"LICENSE_ID" required:"true"`
|
LicenseID string `envconfig:"LICENSE_ID" required:"true"`
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import (
|
|||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connect establishes a connection to NATS with token authentication
|
// Connect establishes a connection to NATS with optional token authentication
|
||||||
// and automatic reconnection handling
|
// and automatic reconnection handling
|
||||||
func Connect(url, token string) (*nats.Conn, error) {
|
func Connect(url, token string) (*nats.Conn, error) {
|
||||||
opts := []nats.Option{
|
opts := []nats.Option{
|
||||||
nats.Token(token),
|
|
||||||
nats.Name("corrosion-companion"),
|
nats.Name("corrosion-companion"),
|
||||||
nats.MaxReconnects(-1), // Unlimited reconnect attempts
|
nats.MaxReconnects(-1),
|
||||||
nats.ReconnectWait(2 * time.Second),
|
nats.ReconnectWait(2 * time.Second),
|
||||||
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
|
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -31,6 +30,11 @@ func Connect(url, token string) (*nats.Conn, error) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only use token auth if a token is provided
|
||||||
|
if token != "" {
|
||||||
|
opts = append(opts, nats.Token(token))
|
||||||
|
}
|
||||||
|
|
||||||
nc, err := nats.Connect(url, opts...)
|
nc, err := nats.Connect(url, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to NATS: %w", err)
|
return nil, fmt.Errorf("failed to connect to NATS: %w", err)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ services:
|
|||||||
DATABASE_MAX_CONNECTIONS: "20"
|
DATABASE_MAX_CONNECTIONS: "20"
|
||||||
NATS_URL: nats://nats:4222
|
NATS_URL: nats://nats:4222
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
JWT_ACCESS_EXPIRY_SECONDS: "900"
|
JWT_ACCESS_EXPIRY_SECONDS: "14400"
|
||||||
JWT_REFRESH_EXPIRY_SECONDS: "604800"
|
JWT_REFRESH_EXPIRY_SECONDS: "604800"
|
||||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||||
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
|
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface AuthResponse {
|
|||||||
refresh_token: string
|
refresh_token: string
|
||||||
requires_totp: boolean
|
requires_totp: boolean
|
||||||
user: User
|
user: User
|
||||||
|
license: License | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerConnection {
|
export interface ServerConnection {
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ chmod +x corrosion-companion-linux-amd64
|
|||||||
# Start with your license key
|
# Start with your license key
|
||||||
export LICENSE_ID="${licenseKey.value}"
|
export LICENSE_ID="${licenseKey.value}"
|
||||||
export NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
export NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
||||||
export NATS_TOKEN="<your-nats-token>"
|
|
||||||
export GAME_SERVER_PATH="/path/to/RustDedicated"
|
|
||||||
./corrosion-companion-linux-amd64`)
|
./corrosion-companion-linux-amd64`)
|
||||||
|
|
||||||
const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt)
|
const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt)
|
||||||
@@ -84,8 +82,6 @@ Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/companion/latest/corrosion
|
|||||||
# Start with your license key
|
# Start with your license key
|
||||||
$env:LICENSE_ID="${licenseKey.value}"
|
$env:LICENSE_ID="${licenseKey.value}"
|
||||||
$env:NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
$env:NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
||||||
$env:NATS_TOKEN="<your-nats-token>"
|
|
||||||
$env:GAME_SERVER_PATH="C:\\RustServer\\server\\RustDedicated.exe"
|
|
||||||
.\\corrosion-companion-windows-amd64.exe`)
|
.\\corrosion-companion-windows-amd64.exe`)
|
||||||
|
|
||||||
async function copySetupCommands() {
|
async function copySetupCommands() {
|
||||||
@@ -351,30 +347,29 @@ onMounted(async () => {
|
|||||||
<Terminal class="w-3.5 h-3.5 text-neutral-500" />
|
<Terminal class="w-3.5 h-3.5 text-neutral-500" />
|
||||||
<p class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Quick Setup</p>
|
<p class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Quick Setup</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<button
|
||||||
<!-- OS Tabs -->
|
@click="copySetupCommands"
|
||||||
<div class="flex bg-neutral-800 rounded-md p-0.5">
|
class="flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-md transition-colors"
|
||||||
<button
|
:class="(setupTab === 'linux' ? copied : windowsCopied)
|
||||||
@click="setupTab = 'linux'"
|
? 'bg-green-600/20 text-green-400 border border-green-600/30'
|
||||||
class="px-3 py-1 text-xs font-medium rounded transition-colors"
|
: 'bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-neutral-200 border border-neutral-700'"
|
||||||
:class="setupTab === 'linux' ? 'bg-neutral-700 text-neutral-100' : 'text-neutral-500 hover:text-neutral-300'"
|
>
|
||||||
>Linux</button>
|
{{ (setupTab === 'linux' ? copied : windowsCopied) ? 'Copied!' : 'Copy' }}
|
||||||
<button
|
</button>
|
||||||
@click="setupTab = 'windows'"
|
</div>
|
||||||
class="px-3 py-1 text-xs font-medium rounded transition-colors"
|
|
||||||
:class="setupTab === 'windows' ? 'bg-neutral-700 text-neutral-100' : 'text-neutral-500 hover:text-neutral-300'"
|
<!-- OS Tabs -->
|
||||||
>Windows</button>
|
<div class="flex bg-neutral-800 rounded-md p-0.5 mb-3 w-fit">
|
||||||
</div>
|
<button
|
||||||
<button
|
@click="setupTab = 'linux'"
|
||||||
@click="copySetupCommands"
|
class="px-3 py-1 text-xs font-medium rounded transition-colors"
|
||||||
class="flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-md transition-colors"
|
:class="setupTab === 'linux' ? 'bg-neutral-700 text-neutral-100' : 'text-neutral-500 hover:text-neutral-300'"
|
||||||
:class="(setupTab === 'linux' ? copied : windowsCopied)
|
>Linux</button>
|
||||||
? 'bg-green-600/20 text-green-400 border border-green-600/30'
|
<button
|
||||||
: 'bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-neutral-200 border border-neutral-700'"
|
@click="setupTab = 'windows'"
|
||||||
>
|
class="px-3 py-1 text-xs font-medium rounded transition-colors"
|
||||||
{{ (setupTab === 'linux' ? copied : windowsCopied) ? 'Copied!' : 'Copy' }}
|
:class="setupTab === 'windows' ? 'bg-neutral-700 text-neutral-100' : 'text-neutral-500 hover:text-neutral-300'"
|
||||||
</button>
|
>Windows</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Windows Warning Badge -->
|
<!-- Windows Warning Badge -->
|
||||||
@@ -391,8 +386,6 @@ onMounted(async () => {
|
|||||||
<p class="mt-3 text-neutral-500"># Start with your license key</p>
|
<p class="mt-3 text-neutral-500"># Start with your license key</p>
|
||||||
<p>export LICENSE_ID=<span class="text-oxide-400">"{{ licenseKey }}"</span></p>
|
<p>export LICENSE_ID=<span class="text-oxide-400">"{{ licenseKey }}"</span></p>
|
||||||
<p>export NATS_URL=<span class="text-oxide-400">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
<p>export NATS_URL=<span class="text-oxide-400">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
||||||
<p>export NATS_TOKEN=<span class="text-neutral-500">"<your-nats-token>"</span></p>
|
|
||||||
<p>export GAME_SERVER_PATH=<span class="text-neutral-500">"/path/to/RustDedicated"</span></p>
|
|
||||||
<p>./corrosion-companion-linux-amd64</p>
|
<p>./corrosion-companion-linux-amd64</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -404,8 +397,6 @@ onMounted(async () => {
|
|||||||
<p class="mt-3 text-neutral-500"># Start with your license key</p>
|
<p class="mt-3 text-neutral-500"># Start with your license key</p>
|
||||||
<p>$env:LICENSE_ID=<span class="text-oxide-400">"{{ licenseKey }}"</span></p>
|
<p>$env:LICENSE_ID=<span class="text-oxide-400">"{{ licenseKey }}"</span></p>
|
||||||
<p>$env:NATS_URL=<span class="text-oxide-400">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
<p>$env:NATS_URL=<span class="text-oxide-400">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
||||||
<p>$env:NATS_TOKEN=<span class="text-neutral-500">"<your-nats-token>"</span></p>
|
|
||||||
<p>$env:GAME_SERVER_PATH=<span class="text-neutral-500">"C:\RustServer\server\RustDedicated.exe"</span></p>
|
|
||||||
<p>.\corrosion-companion-windows-amd64.exe</p>
|
<p>.\corrosion-companion-windows-amd64.exe</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ async function handleLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authStore.setAuth(response)
|
authStore.setAuth(response)
|
||||||
|
if (response.license) {
|
||||||
|
authStore.setLicense(response.license)
|
||||||
|
}
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
@@ -68,6 +71,9 @@ async function handleTotpVerify() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
authStore.setAuth(response)
|
authStore.setAuth(response)
|
||||||
|
if (response.license) {
|
||||||
|
authStore.setLicense(response.license)
|
||||||
|
}
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
totpCode.value = ''
|
totpCode.value = ''
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ async function handleRegister() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
authStore.setAuth(response)
|
authStore.setAuth(response)
|
||||||
|
if (response.license) {
|
||||||
|
authStore.setLicense(response.license)
|
||||||
|
}
|
||||||
router.push('/setup')
|
router.push('/setup')
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ interface License {
|
|||||||
id: string
|
id: string
|
||||||
license_key: string
|
license_key: string
|
||||||
owner_email: string
|
owner_email: string
|
||||||
server_name: string
|
server_name: string | null
|
||||||
status: 'active' | 'suspended' | 'expired' | 'revoked'
|
status: 'active' | 'suspended' | 'expired' | 'revoked'
|
||||||
created_at: string
|
created_at: string
|
||||||
expires_at: string
|
expires_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LicenseDetail {
|
interface LicenseDetail {
|
||||||
id: string
|
id: string
|
||||||
license_key: string
|
license_key: string
|
||||||
owner_email: string
|
owner_email: string
|
||||||
server_name: string
|
server_name: string | null
|
||||||
status: string
|
status: string
|
||||||
created_at: string
|
created_at: string
|
||||||
expires_at: string
|
expires_at: string | null
|
||||||
team_count: number
|
team_count: number
|
||||||
wipe_count: number
|
wipe_count: number
|
||||||
server_connection: {
|
server_connection: {
|
||||||
@@ -67,8 +67,11 @@ const statusBadgeClass: Record<string, string> = {
|
|||||||
revoked: 'bg-red-500/10 text-red-400',
|
revoked: 'bg-red-500/10 text-red-400',
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(iso: string): string {
|
function formatDate(iso: string | null | undefined): string {
|
||||||
return new Date(iso).toLocaleDateString('en-US', {
|
if (!iso) return '—'
|
||||||
|
const d = new Date(iso)
|
||||||
|
if (isNaN(d.getTime()) || d.getTime() === 0) return '—'
|
||||||
|
return d.toLocaleDateString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -80,7 +83,7 @@ async function fetchLicenses() {
|
|||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: page.value.toString(),
|
page: page.value.toString(),
|
||||||
per_page: perPage.toString(),
|
limit: perPage.toString(),
|
||||||
})
|
})
|
||||||
if (searchQuery.value.trim()) params.set('search', searchQuery.value.trim())
|
if (searchQuery.value.trim()) params.set('search', searchQuery.value.trim())
|
||||||
if (statusFilter.value !== 'all') params.set('status', statusFilter.value)
|
if (statusFilter.value !== 'all') params.set('status', statusFilter.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user