测试
This commit is contained in:
parent
ff6739ab68
commit
aa1346f6af
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
DATABASE_URL="postgresql://postgres:lyh1234@192.168.0.180:5432/tyt-api-nest"
|
||||||
|
AUTH_TOKEN_SECRET="replace-with-a-strong-random-secret"
|
||||||
|
SYSTEM_ADMIN_BOOTSTRAP_KEY="replace-with-admin-bootstrap-key"
|
||||||
@ -17,10 +17,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@prisma/adapter-pg": "^7.5.0",
|
"@prisma/adapter-pg": "^7.5.0",
|
||||||
"@prisma/client": "^7.5.0",
|
"@prisma/client": "^7.5.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
@ -29,7 +33,9 @@
|
|||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
|||||||
163
pnpm-lock.yaml
generated
163
pnpm-lock.yaml
generated
@ -14,6 +14,9 @@ importers:
|
|||||||
'@nestjs/core':
|
'@nestjs/core':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/event-emitter':
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)
|
||||||
'@nestjs/mapped-types':
|
'@nestjs/mapped-types':
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: 2.1.0(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)
|
version: 2.1.0(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)
|
||||||
@ -26,6 +29,15 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^7.5.0
|
specifier: ^7.5.0
|
||||||
version: 7.5.0(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
|
version: 7.5.0(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
|
||||||
|
bcrypt:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
|
dotenv:
|
||||||
|
specifier: ^17.3.1
|
||||||
|
version: 17.3.1
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.3
|
||||||
|
version: 9.0.3
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.20.0
|
specifier: ^8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
@ -45,9 +57,15 @@ importers:
|
|||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(@nestjs/platform-express@11.1.16)
|
version: 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)(@nestjs/platform-express@11.1.16)
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.10
|
||||||
|
version: 9.0.10
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.7
|
specifier: ^22.10.7
|
||||||
version: 22.19.15
|
version: 22.19.15
|
||||||
@ -380,6 +398,12 @@ packages:
|
|||||||
'@nestjs/websockets':
|
'@nestjs/websockets':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/event-emitter@3.0.1':
|
||||||
|
resolution: {integrity: sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/core': ^10.0.0 || ^11.0.0
|
||||||
|
|
||||||
'@nestjs/mapped-types@2.1.0':
|
'@nestjs/mapped-types@2.1.0':
|
||||||
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
|
resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -512,6 +536,9 @@ packages:
|
|||||||
'@tsconfig/node16@1.0.4':
|
'@tsconfig/node16@1.0.4':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@ -542,9 +569,15 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
'@types/methods@1.1.4':
|
'@types/methods@1.1.4':
|
||||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@22.19.15':
|
'@types/node@22.19.15':
|
||||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||||
|
|
||||||
@ -730,6 +763,10 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
@ -753,6 +790,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@ -967,10 +1007,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dotenv@17.3.1:
|
||||||
|
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@ -1049,6 +1096,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
eventemitter2@6.4.9:
|
||||||
|
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
|
||||||
|
|
||||||
events@3.3.0:
|
events@3.3.0:
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
@ -1279,6 +1329,16 @@ packages:
|
|||||||
jsonfile@6.2.0:
|
jsonfile@6.2.0:
|
||||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.3:
|
||||||
|
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jwa@2.0.1:
|
||||||
|
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||||
|
|
||||||
|
jws@4.0.1:
|
||||||
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
lilconfig@2.1.0:
|
lilconfig@2.1.0:
|
||||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1294,6 +1354,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
|
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
|
||||||
engines: {node: '>=6.11.5'}
|
engines: {node: '>=6.11.5'}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6:
|
||||||
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
|
||||||
|
lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
lodash@4.17.21:
|
lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
@ -1420,12 +1501,20 @@ packages:
|
|||||||
node-abort-controller@3.1.1:
|
node-abort-controller@3.1.1:
|
||||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
||||||
|
|
||||||
|
node-addon-api@8.6.0:
|
||||||
|
resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==}
|
||||||
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
||||||
|
|
||||||
node-fetch-native@1.6.7:
|
node-fetch-native@1.6.7:
|
||||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4:
|
||||||
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-releases@2.0.36:
|
node-releases@2.0.36:
|
||||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||||
|
|
||||||
@ -2350,6 +2439,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nestjs/platform-express': 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)
|
'@nestjs/platform-express': 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)
|
||||||
|
|
||||||
|
'@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.16)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.16(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.16)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
eventemitter2: 6.4.9
|
||||||
|
|
||||||
'@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)':
|
'@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.16(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@ -2505,6 +2600,10 @@ snapshots:
|
|||||||
|
|
||||||
'@tsconfig/node16@1.0.4': {}
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.15
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@ -2545,8 +2644,15 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
'@types/node': 22.19.15
|
||||||
|
|
||||||
'@types/methods@1.1.4': {}
|
'@types/methods@1.1.4': {}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@22.19.15':
|
'@types/node@22.19.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
@ -2751,6 +2857,11 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.10.0: {}
|
baseline-browser-mapping@2.10.0: {}
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-addon-api: 8.6.0
|
||||||
|
node-gyp-build: 4.8.4
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
@ -2792,6 +2903,8 @@ snapshots:
|
|||||||
node-releases: 2.0.36
|
node-releases: 2.0.36
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
@ -2980,12 +3093,18 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.6.1: {}
|
dotenv@16.6.1: {}
|
||||||
|
|
||||||
|
dotenv@17.3.1: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
effect@3.18.4:
|
effect@3.18.4:
|
||||||
@ -3048,6 +3167,8 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
eventemitter2@6.4.9: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
express@5.2.1:
|
express@5.2.1:
|
||||||
@ -3309,6 +3430,30 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.3:
|
||||||
|
dependencies:
|
||||||
|
jws: 4.0.1
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.7.4
|
||||||
|
|
||||||
|
jwa@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jws@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
jwa: 2.0.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
lilconfig@2.1.0: {}
|
lilconfig@2.1.0: {}
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
@ -3317,6 +3462,20 @@ snapshots:
|
|||||||
|
|
||||||
loader-runner@4.3.1: {}
|
loader-runner@4.3.1: {}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0: {}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4: {}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1: {}
|
||||||
|
|
||||||
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
lodash@4.17.23: {}
|
lodash@4.17.23: {}
|
||||||
@ -3420,12 +3579,16 @@ snapshots:
|
|||||||
|
|
||||||
node-abort-controller@3.1.1: {}
|
node-abort-controller@3.1.1: {}
|
||||||
|
|
||||||
|
node-addon-api@8.6.0: {}
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.23
|
lodash: 4.17.23
|
||||||
|
|
||||||
node-fetch-native@1.6.7: {}
|
node-fetch-native@1.6.7: {}
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-releases@2.0.36: {}
|
node-releases@2.0.36: {}
|
||||||
|
|
||||||
nypm@0.6.5:
|
nypm@0.6.5:
|
||||||
|
|||||||
@ -1,9 +1,3 @@
|
|||||||
// This is your Prisma schema file,
|
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
||||||
|
|
||||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
||||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client"
|
provider = "prisma-client"
|
||||||
output = "../src/generated/prisma"
|
output = "../src/generated/prisma"
|
||||||
@ -13,18 +7,132 @@ datasource db {
|
|||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
enum Role {
|
||||||
id Int @id @default(autoincrement())
|
SYSTEM_ADMIN
|
||||||
email String @unique
|
HOSPITAL_ADMIN
|
||||||
name String?
|
DIRECTOR
|
||||||
posts Post[]
|
LEADER
|
||||||
|
DOCTOR
|
||||||
|
ENGINEER
|
||||||
}
|
}
|
||||||
|
|
||||||
model Post {
|
enum DeviceStatus {
|
||||||
id Int @id @default(autoincrement())
|
ACTIVE
|
||||||
title String
|
INACTIVE
|
||||||
content String?
|
}
|
||||||
published Boolean? @default(false)
|
|
||||||
author User? @relation(fields: [authorId], references: [id])
|
enum TaskStatus {
|
||||||
authorId Int?
|
PENDING
|
||||||
|
ACCEPTED
|
||||||
|
COMPLETED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
model Hospital {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
departments Department[]
|
||||||
|
users User[]
|
||||||
|
patients Patient[]
|
||||||
|
tasks Task[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Department {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
hospitalId Int
|
||||||
|
hospital Hospital @relation(fields: [hospitalId], references: [id])
|
||||||
|
groups Group[]
|
||||||
|
users User[]
|
||||||
|
|
||||||
|
@@index([hospitalId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Group {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
departmentId Int
|
||||||
|
department Department @relation(fields: [departmentId], references: [id])
|
||||||
|
users User[]
|
||||||
|
|
||||||
|
@@index([departmentId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
phone String
|
||||||
|
// Backend login password hash (bcrypt).
|
||||||
|
passwordHash String?
|
||||||
|
openId String? @unique
|
||||||
|
role Role
|
||||||
|
hospitalId Int?
|
||||||
|
departmentId Int?
|
||||||
|
groupId Int?
|
||||||
|
hospital Hospital? @relation(fields: [hospitalId], references: [id])
|
||||||
|
department Department? @relation(fields: [departmentId], references: [id])
|
||||||
|
group Group? @relation(fields: [groupId], references: [id])
|
||||||
|
doctorPatients Patient[] @relation("DoctorPatients")
|
||||||
|
createdTasks Task[] @relation("TaskCreator")
|
||||||
|
acceptedTasks Task[] @relation("TaskEngineer")
|
||||||
|
|
||||||
|
@@index([phone])
|
||||||
|
@@index([hospitalId, role])
|
||||||
|
@@index([departmentId, role])
|
||||||
|
@@index([groupId, role])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Patient {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
phone String
|
||||||
|
idCardHash String
|
||||||
|
hospitalId Int
|
||||||
|
doctorId Int
|
||||||
|
hospital Hospital @relation(fields: [hospitalId], references: [id])
|
||||||
|
doctor User @relation("DoctorPatients", fields: [doctorId], references: [id])
|
||||||
|
devices Device[]
|
||||||
|
|
||||||
|
@@index([phone, idCardHash])
|
||||||
|
@@index([hospitalId, doctorId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Device {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
snCode String @unique
|
||||||
|
currentPressure Int
|
||||||
|
status DeviceStatus @default(ACTIVE)
|
||||||
|
patientId Int
|
||||||
|
patient Patient @relation(fields: [patientId], references: [id])
|
||||||
|
taskItems TaskItem[]
|
||||||
|
|
||||||
|
@@index([patientId, status])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Task {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
status TaskStatus @default(PENDING)
|
||||||
|
creatorId Int
|
||||||
|
engineerId Int?
|
||||||
|
hospitalId Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
creator User @relation("TaskCreator", fields: [creatorId], references: [id])
|
||||||
|
engineer User? @relation("TaskEngineer", fields: [engineerId], references: [id])
|
||||||
|
hospital Hospital @relation(fields: [hospitalId], references: [id])
|
||||||
|
items TaskItem[]
|
||||||
|
|
||||||
|
@@index([hospitalId, status, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model TaskItem {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
taskId Int
|
||||||
|
deviceId Int
|
||||||
|
oldPressure Int
|
||||||
|
targetPressure Int
|
||||||
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||||||
|
device Device @relation(fields: [deviceId], references: [id])
|
||||||
|
|
||||||
|
@@index([taskId])
|
||||||
|
@@index([deviceId])
|
||||||
}
|
}
|
||||||
|
|||||||
202
prisma/seed.mjs
Normal file
202
prisma/seed.mjs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { hash } from 'bcrypt';
|
||||||
|
import { PrismaClient } from '../src/generated/prisma/client.js';
|
||||||
|
import { DeviceStatus, Role } from '../src/generated/prisma/enums.js';
|
||||||
|
|
||||||
|
// Keep the seed executable with the same pg driver adapter used by PrismaService.
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error('DATABASE_URL is required to run seed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = new PrismaClient({
|
||||||
|
adapter: new PrismaPg({ connectionString }),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Default seed login password (plain): Seed@1234
|
||||||
|
const seedPasswordHash = await hash('Seed@1234', 12);
|
||||||
|
|
||||||
|
// Seed a baseline organization tree for local/demo usage.
|
||||||
|
const hospital =
|
||||||
|
(await prisma.hospital.findFirst({ where: { name: 'Demo Hospital' } })) ??
|
||||||
|
(await prisma.hospital.create({
|
||||||
|
data: { name: 'Demo Hospital' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const department =
|
||||||
|
(await prisma.department.findFirst({
|
||||||
|
where: {
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
name: 'Neurosurgery',
|
||||||
|
},
|
||||||
|
})) ??
|
||||||
|
(await prisma.department.create({
|
||||||
|
data: {
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
name: 'Neurosurgery',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const group =
|
||||||
|
(await prisma.group.findFirst({
|
||||||
|
where: {
|
||||||
|
departmentId: department.id,
|
||||||
|
name: 'Shift-A',
|
||||||
|
},
|
||||||
|
})) ??
|
||||||
|
(await prisma.group.create({
|
||||||
|
data: {
|
||||||
|
departmentId: department.id,
|
||||||
|
name: 'Shift-A',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Use openId as idempotent unique key for seeded users.
|
||||||
|
const systemAdmin = await prisma.user.upsert({
|
||||||
|
where: { openId: 'seed-system-admin-openid' },
|
||||||
|
update: {
|
||||||
|
name: 'System Admin',
|
||||||
|
phone: '13800000000',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
role: Role.SYSTEM_ADMIN,
|
||||||
|
hospitalId: null,
|
||||||
|
departmentId: null,
|
||||||
|
groupId: null,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: 'System Admin',
|
||||||
|
phone: '13800000000',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
openId: 'seed-system-admin-openid',
|
||||||
|
role: Role.SYSTEM_ADMIN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { openId: 'seed-hospital-admin-openid' },
|
||||||
|
update: {
|
||||||
|
name: 'Hospital Admin',
|
||||||
|
phone: '13800000001',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
role: Role.HOSPITAL_ADMIN,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
groupId: group.id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: 'Hospital Admin',
|
||||||
|
phone: '13800000001',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
openId: 'seed-hospital-admin-openid',
|
||||||
|
role: Role.HOSPITAL_ADMIN,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
groupId: group.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const doctor = await prisma.user.upsert({
|
||||||
|
where: { openId: 'seed-doctor-openid' },
|
||||||
|
update: {
|
||||||
|
name: 'Doctor Demo',
|
||||||
|
phone: '13800000002',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
role: Role.DOCTOR,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
groupId: group.id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: 'Doctor Demo',
|
||||||
|
phone: '13800000002',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
openId: 'seed-doctor-openid',
|
||||||
|
role: Role.DOCTOR,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
groupId: group.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { openId: 'seed-engineer-openid' },
|
||||||
|
update: {
|
||||||
|
name: 'Engineer Demo',
|
||||||
|
phone: '13800000009',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
role: Role.ENGINEER,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
departmentId: null,
|
||||||
|
groupId: null,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: 'Engineer Demo',
|
||||||
|
phone: '13800000009',
|
||||||
|
passwordHash: seedPasswordHash,
|
||||||
|
openId: 'seed-engineer-openid',
|
||||||
|
role: Role.ENGINEER,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const patient =
|
||||||
|
(await prisma.patient.findFirst({
|
||||||
|
where: {
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
phone: '13800000003',
|
||||||
|
idCardHash: 'seed-id-card-hash',
|
||||||
|
},
|
||||||
|
})) ??
|
||||||
|
(await prisma.patient.create({
|
||||||
|
data: {
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
doctorId: doctor.id,
|
||||||
|
name: 'Patient Demo',
|
||||||
|
phone: '13800000003',
|
||||||
|
idCardHash: 'seed-id-card-hash',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
await prisma.device.upsert({
|
||||||
|
where: { snCode: 'SEED-SN-001' },
|
||||||
|
update: {
|
||||||
|
patientId: patient.id,
|
||||||
|
currentPressure: 110,
|
||||||
|
status: DeviceStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
snCode: 'SEED-SN-001',
|
||||||
|
patientId: patient.id,
|
||||||
|
currentPressure: 110,
|
||||||
|
status: DeviceStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
ok: true,
|
||||||
|
hospitalId: hospital.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
groupId: group.id,
|
||||||
|
systemAdminId: systemAdmin.id,
|
||||||
|
doctorId: doctor.id,
|
||||||
|
patientId: patient.id,
|
||||||
|
seedPasswordPlain: 'Seed@1234',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Seed failed:', error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@ -1,7 +1,19 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UsersModule } from './users/users.module';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { PrismaModule } from './prisma.module.js';
|
||||||
|
import { UsersModule } from './users/users.module.js';
|
||||||
|
import { TasksModule } from './tasks/tasks.module.js';
|
||||||
|
import { PatientsModule } from './patients/patients.module.js';
|
||||||
|
import { AuthModule } from './auth/auth.module.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
UsersModule,
|
||||||
|
TasksModule,
|
||||||
|
PatientsModule,
|
||||||
|
AuthModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
86
src/auth/access-token.guard.ts
Normal file
86
src/auth/access-token.guard.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Role } from '../generated/prisma/enums.js';
|
||||||
|
import { ActorContext } from '../common/actor-context.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccessTokenGuard implements CanActivate {
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const request = context.switchToHttp().getRequest<
|
||||||
|
{
|
||||||
|
headers: Record<string, string | string[] | undefined>;
|
||||||
|
actor?: unknown;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
const authorization = request.headers.authorization;
|
||||||
|
const headerValue = Array.isArray(authorization)
|
||||||
|
? authorization[0]
|
||||||
|
: authorization;
|
||||||
|
|
||||||
|
if (!headerValue || !headerValue.startsWith('Bearer ')) {
|
||||||
|
throw new UnauthorizedException('Missing bearer token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = headerValue.slice('Bearer '.length).trim();
|
||||||
|
request.actor = this.verifyAndExtractActor(token);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private verifyAndExtractActor(token: string): ActorContext {
|
||||||
|
const secret = process.env.AUTH_TOKEN_SECRET;
|
||||||
|
if (!secret) {
|
||||||
|
throw new UnauthorizedException('AUTH_TOKEN_SECRET is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: string | jwt.JwtPayload;
|
||||||
|
try {
|
||||||
|
payload = jwt.verify(token, secret, {
|
||||||
|
algorithms: ['HS256'],
|
||||||
|
issuer: 'tyt-api-nest',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException('Invalid or expired token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload !== 'object') {
|
||||||
|
throw new UnauthorizedException('Invalid token payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = payload.role;
|
||||||
|
if (typeof role !== 'string' || !Object.values(Role).includes(role as Role)) {
|
||||||
|
throw new UnauthorizedException('Invalid role in token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.asInt(payload.id, 'id'),
|
||||||
|
role: role as Role,
|
||||||
|
hospitalId: this.asNullableInt(payload.hospitalId, 'hospitalId'),
|
||||||
|
departmentId: this.asNullableInt(payload.departmentId, 'departmentId'),
|
||||||
|
groupId: this.asNullableInt(payload.groupId, 'groupId'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private asInt(value: unknown, field: string): number {
|
||||||
|
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
||||||
|
throw new UnauthorizedException(`Invalid ${field} in token`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private asNullableInt(value: unknown, field: string): number | null {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
||||||
|
throw new UnauthorizedException(`Invalid ${field} in token`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/auth/auth.controller.ts
Normal file
28
src/auth/auth.controller.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service.js';
|
||||||
|
import { RegisterUserDto } from '../users/dto/register-user.dto.js';
|
||||||
|
import { LoginDto } from '../users/dto/login.dto.js';
|
||||||
|
import { AccessTokenGuard } from './access-token.guard.js';
|
||||||
|
import { CurrentActor } from './current-actor.decorator.js';
|
||||||
|
import type { ActorContext } from '../common/actor-context.js';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
register(@Body() dto: RegisterUserDto) {
|
||||||
|
return this.authService.register(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('login')
|
||||||
|
login(@Body() dto: LoginDto) {
|
||||||
|
return this.authService.login(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('me')
|
||||||
|
@UseGuards(AccessTokenGuard)
|
||||||
|
me(@CurrentActor() actor: ActorContext) {
|
||||||
|
return this.authService.me(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/auth/auth.module.ts
Normal file
13
src/auth/auth.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service.js';
|
||||||
|
import { AuthController } from './auth.controller.js';
|
||||||
|
import { UsersModule } from '../users/users.module.js';
|
||||||
|
import { AccessTokenGuard } from './access-token.guard.js';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [UsersModule],
|
||||||
|
providers: [AuthService, AccessTokenGuard],
|
||||||
|
controllers: [AuthController],
|
||||||
|
exports: [AuthService, AccessTokenGuard],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
22
src/auth/auth.service.ts
Normal file
22
src/auth/auth.service.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ActorContext } from '../common/actor-context.js';
|
||||||
|
import { UsersService } from '../users/users.service.js';
|
||||||
|
import { RegisterUserDto } from '../users/dto/register-user.dto.js';
|
||||||
|
import { LoginDto } from '../users/dto/login.dto.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
register(dto: RegisterUserDto) {
|
||||||
|
return this.usersService.register(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
login(dto: LoginDto) {
|
||||||
|
return this.usersService.login(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
me(actor: ActorContext) {
|
||||||
|
return this.usersService.me(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/auth/current-actor.decorator.ts
Normal file
9
src/auth/current-actor.decorator.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { ActorContext } from '../common/actor-context.js';
|
||||||
|
|
||||||
|
export const CurrentActor = createParamDecorator(
|
||||||
|
(_data: unknown, context: ExecutionContext): ActorContext => {
|
||||||
|
const request = context.switchToHttp().getRequest<{ actor: ActorContext }>();
|
||||||
|
return request.actor;
|
||||||
|
},
|
||||||
|
);
|
||||||
5
src/auth/roles.decorator.ts
Normal file
5
src/auth/roles.decorator.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { Role } from '../generated/prisma/enums.js';
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
33
src/auth/roles.guard.ts
Normal file
33
src/auth/roles.guard.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Role } from '../generated/prisma/enums.js';
|
||||||
|
import { ROLES_KEY } from './roles.decorator.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private readonly reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!requiredRoles || requiredRoles.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest<{ actor?: { role?: Role } }>();
|
||||||
|
const actorRole = request.actor?.role;
|
||||||
|
if (!actorRole || !requiredRoles.includes(actorRole)) {
|
||||||
|
throw new ForbiddenException('Role is not allowed for this endpoint');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/common/actor-context.ts
Normal file
9
src/common/actor-context.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Role } from '../generated/prisma/enums.js';
|
||||||
|
|
||||||
|
export type ActorContext = {
|
||||||
|
id: number;
|
||||||
|
role: Role;
|
||||||
|
hospitalId: number | null;
|
||||||
|
departmentId: number | null;
|
||||||
|
groupId: number | null;
|
||||||
|
};
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module.js';
|
import { AppModule } from './app.module.js';
|
||||||
|
|
||||||
|
|||||||
32
src/patients/b-patients/b-patients.controller.ts
Normal file
32
src/patients/b-patients/b-patients.controller.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import type { ActorContext } from '../../common/actor-context.js';
|
||||||
|
import { CurrentActor } from '../../auth/current-actor.decorator.js';
|
||||||
|
import { AccessTokenGuard } from '../../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../../auth/roles.guard.js';
|
||||||
|
import { Roles } from '../../auth/roles.decorator.js';
|
||||||
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
import { PatientService } from '../patient/patient.service.js';
|
||||||
|
|
||||||
|
@Controller('b/patients')
|
||||||
|
@UseGuards(AccessTokenGuard, RolesGuard)
|
||||||
|
export class BPatientsController {
|
||||||
|
constructor(private readonly patientService: PatientService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(
|
||||||
|
Role.SYSTEM_ADMIN,
|
||||||
|
Role.HOSPITAL_ADMIN,
|
||||||
|
Role.DIRECTOR,
|
||||||
|
Role.LEADER,
|
||||||
|
Role.DOCTOR,
|
||||||
|
)
|
||||||
|
findVisiblePatients(
|
||||||
|
@CurrentActor() actor: ActorContext,
|
||||||
|
@Query('hospitalId') hospitalId?: string,
|
||||||
|
) {
|
||||||
|
const requestedHospitalId =
|
||||||
|
hospitalId == null || hospitalId === '' ? undefined : Number(hospitalId);
|
||||||
|
|
||||||
|
return this.patientService.findPatientsForB(actor, requestedHospitalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/patients/c-patients/c-patients.controller.ts
Normal file
16
src/patients/c-patients/c-patients.controller.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { PatientService } from '../patient/patient.service.js';
|
||||||
|
import { FamilyLifecycleQueryDto } from '../dto/family-lifecycle-query.dto.js';
|
||||||
|
|
||||||
|
@Controller('c/patients')
|
||||||
|
export class CPatientsController {
|
||||||
|
constructor(private readonly patientService: PatientService) {}
|
||||||
|
|
||||||
|
@Get('lifecycle')
|
||||||
|
getLifecycle(@Query() query: FamilyLifecycleQueryDto) {
|
||||||
|
return this.patientService.getFamilyLifecycleByIdentity(
|
||||||
|
query.phone,
|
||||||
|
query.idCardHash,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/patients/dto/family-lifecycle-query.dto.ts
Normal file
4
src/patients/dto/family-lifecycle-query.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class FamilyLifecycleQueryDto {
|
||||||
|
phone!: string;
|
||||||
|
idCardHash!: string;
|
||||||
|
}
|
||||||
154
src/patients/patient/patient.service.ts
Normal file
154
src/patients/patient/patient.service.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
import { PrismaService } from '../../prisma.service.js';
|
||||||
|
import { ActorContext } from '../../common/actor-context.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PatientService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findPatientsForB(actor: ActorContext, requestedHospitalId?: number) {
|
||||||
|
const hospitalId = this.resolveHospitalId(actor, requestedHospitalId);
|
||||||
|
|
||||||
|
const where: Record<string, unknown> = { hospitalId };
|
||||||
|
switch (actor.role) {
|
||||||
|
case Role.DOCTOR:
|
||||||
|
where.doctorId = actor.id;
|
||||||
|
break;
|
||||||
|
case Role.LEADER:
|
||||||
|
if (!actor.groupId) {
|
||||||
|
throw new BadRequestException('Actor groupId is required for LEADER');
|
||||||
|
}
|
||||||
|
where.doctor = {
|
||||||
|
groupId: actor.groupId,
|
||||||
|
role: Role.DOCTOR,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case Role.DIRECTOR:
|
||||||
|
if (!actor.departmentId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Actor departmentId is required for DIRECTOR',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
where.doctor = {
|
||||||
|
departmentId: actor.departmentId,
|
||||||
|
role: Role.DOCTOR,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case Role.HOSPITAL_ADMIN:
|
||||||
|
case Role.SYSTEM_ADMIN:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ForbiddenException('Role cannot query B-side patient list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.patient.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
hospital: { select: { id: true, name: true } },
|
||||||
|
doctor: { select: { id: true, name: true, role: true } },
|
||||||
|
devices: true,
|
||||||
|
},
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFamilyLifecycleByIdentity(phone: string, idCardHash: string) {
|
||||||
|
if (!phone || !idCardHash) {
|
||||||
|
throw new BadRequestException('phone and idCardHash are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const patients = await this.prisma.patient.findMany({
|
||||||
|
where: {
|
||||||
|
phone,
|
||||||
|
idCardHash,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
hospital: { select: { id: true, name: true } },
|
||||||
|
devices: {
|
||||||
|
include: {
|
||||||
|
taskItems: {
|
||||||
|
include: {
|
||||||
|
task: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const lifecycle = patients
|
||||||
|
.flatMap((patient) =>
|
||||||
|
patient.devices.flatMap((device) =>
|
||||||
|
device.taskItems.map((taskItem) => ({
|
||||||
|
eventType: 'TASK_PRESSURE_ADJUSTMENT',
|
||||||
|
occurredAt: taskItem.task.createdAt,
|
||||||
|
hospital: patient.hospital,
|
||||||
|
patient: {
|
||||||
|
id: patient.id,
|
||||||
|
name: patient.name,
|
||||||
|
phone: patient.phone,
|
||||||
|
},
|
||||||
|
device: {
|
||||||
|
id: device.id,
|
||||||
|
snCode: device.snCode,
|
||||||
|
status: device.status,
|
||||||
|
currentPressure: device.currentPressure,
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
id: taskItem.task.id,
|
||||||
|
status: taskItem.task.status,
|
||||||
|
creatorId: taskItem.task.creatorId,
|
||||||
|
engineerId: taskItem.task.engineerId,
|
||||||
|
hospitalId: taskItem.task.hospitalId,
|
||||||
|
createdAt: taskItem.task.createdAt,
|
||||||
|
},
|
||||||
|
taskItem: {
|
||||||
|
id: taskItem.id,
|
||||||
|
oldPressure: taskItem.oldPressure,
|
||||||
|
targetPressure: taskItem.targetPressure,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
phone,
|
||||||
|
idCardHash,
|
||||||
|
patientCount: patients.length,
|
||||||
|
lifecycle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveHospitalId(
|
||||||
|
actor: ActorContext,
|
||||||
|
requestedHospitalId?: number,
|
||||||
|
): number {
|
||||||
|
if (actor.role === Role.SYSTEM_ADMIN) {
|
||||||
|
const normalizedHospitalId = requestedHospitalId;
|
||||||
|
if (
|
||||||
|
normalizedHospitalId == null ||
|
||||||
|
!Number.isInteger(normalizedHospitalId)
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'SYSTEM_ADMIN must pass hospitalId query parameter',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return normalizedHospitalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actor.hospitalId) {
|
||||||
|
throw new BadRequestException('Actor hospitalId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return actor.hospitalId;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/patients/patients.module.ts
Normal file
13
src/patients/patients.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PatientService } from './patient/patient.service.js';
|
||||||
|
import { BPatientsController } from './b-patients/b-patients.controller.js';
|
||||||
|
import { CPatientsController } from './c-patients/c-patients.controller.js';
|
||||||
|
import { AccessTokenGuard } from '../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../auth/roles.guard.js';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PatientService, AccessTokenGuard, RolesGuard],
|
||||||
|
controllers: [BPatientsController, CPatientsController],
|
||||||
|
exports: [PatientService],
|
||||||
|
})
|
||||||
|
export class PatientsModule {}
|
||||||
9
src/prisma.module.ts
Normal file
9
src/prisma.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service.js';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
42
src/tasks/b-tasks/b-tasks.controller.ts
Normal file
42
src/tasks/b-tasks/b-tasks.controller.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import type { ActorContext } from '../../common/actor-context.js';
|
||||||
|
import { CurrentActor } from '../../auth/current-actor.decorator.js';
|
||||||
|
import { AccessTokenGuard } from '../../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../../auth/roles.guard.js';
|
||||||
|
import { Roles } from '../../auth/roles.decorator.js';
|
||||||
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
import { TaskService } from '../task.service.js';
|
||||||
|
import { PublishTaskDto } from '../dto/publish-task.dto.js';
|
||||||
|
import { AcceptTaskDto } from '../dto/accept-task.dto.js';
|
||||||
|
import { CompleteTaskDto } from '../dto/complete-task.dto.js';
|
||||||
|
import { CancelTaskDto } from '../dto/cancel-task.dto.js';
|
||||||
|
|
||||||
|
@Controller('b/tasks')
|
||||||
|
@UseGuards(AccessTokenGuard, RolesGuard)
|
||||||
|
export class BTasksController {
|
||||||
|
constructor(private readonly taskService: TaskService) {}
|
||||||
|
|
||||||
|
@Post('publish')
|
||||||
|
@Roles(Role.DOCTOR)
|
||||||
|
publish(@CurrentActor() actor: ActorContext, @Body() dto: PublishTaskDto) {
|
||||||
|
return this.taskService.publishTask(actor, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('accept')
|
||||||
|
@Roles(Role.ENGINEER)
|
||||||
|
accept(@CurrentActor() actor: ActorContext, @Body() dto: AcceptTaskDto) {
|
||||||
|
return this.taskService.acceptTask(actor, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('complete')
|
||||||
|
@Roles(Role.ENGINEER)
|
||||||
|
complete(@CurrentActor() actor: ActorContext, @Body() dto: CompleteTaskDto) {
|
||||||
|
return this.taskService.completeTask(actor, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('cancel')
|
||||||
|
@Roles(Role.DOCTOR)
|
||||||
|
cancel(@CurrentActor() actor: ActorContext, @Body() dto: CancelTaskDto) {
|
||||||
|
return this.taskService.cancelTask(actor, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/tasks/dto/accept-task.dto.ts
Normal file
3
src/tasks/dto/accept-task.dto.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class AcceptTaskDto {
|
||||||
|
taskId!: number;
|
||||||
|
}
|
||||||
3
src/tasks/dto/cancel-task.dto.ts
Normal file
3
src/tasks/dto/cancel-task.dto.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class CancelTaskDto {
|
||||||
|
taskId!: number;
|
||||||
|
}
|
||||||
3
src/tasks/dto/complete-task.dto.ts
Normal file
3
src/tasks/dto/complete-task.dto.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class CompleteTaskDto {
|
||||||
|
taskId!: number;
|
||||||
|
}
|
||||||
9
src/tasks/dto/publish-task.dto.ts
Normal file
9
src/tasks/dto/publish-task.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class PublishTaskItemDto {
|
||||||
|
deviceId!: number;
|
||||||
|
targetPressure!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PublishTaskDto {
|
||||||
|
engineerId?: number;
|
||||||
|
items!: PublishTaskItemDto[];
|
||||||
|
}
|
||||||
263
src/tasks/task.service.ts
Normal file
263
src/tasks/task.service.ts
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { DeviceStatus, Role, TaskStatus } from '../generated/prisma/enums.js';
|
||||||
|
import { PrismaService } from '../prisma.service.js';
|
||||||
|
import { ActorContext } from '../common/actor-context.js';
|
||||||
|
import { PublishTaskDto } from './dto/publish-task.dto.js';
|
||||||
|
import { AcceptTaskDto } from './dto/accept-task.dto.js';
|
||||||
|
import { CompleteTaskDto } from './dto/complete-task.dto.js';
|
||||||
|
import { CancelTaskDto } from './dto/cancel-task.dto.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TaskService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async publishTask(actor: ActorContext, dto: PublishTaskDto) {
|
||||||
|
this.assertRole(actor, [Role.DOCTOR]);
|
||||||
|
const hospitalId = this.requireHospitalId(actor);
|
||||||
|
|
||||||
|
if (!Array.isArray(dto.items) || dto.items.length === 0) {
|
||||||
|
throw new BadRequestException('items is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
dto.items.map((item) => {
|
||||||
|
if (!Number.isInteger(item.deviceId)) {
|
||||||
|
throw new BadRequestException(`Invalid deviceId: ${item.deviceId}`);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(item.targetPressure)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid targetPressure: ${item.targetPressure}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return item.deviceId;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const devices = await this.prisma.device.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: deviceIds },
|
||||||
|
status: DeviceStatus.ACTIVE,
|
||||||
|
patient: { hospitalId },
|
||||||
|
},
|
||||||
|
select: { id: true, currentPressure: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (devices.length !== deviceIds.length) {
|
||||||
|
throw new NotFoundException('Some devices are not found in actor hospital');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.engineerId != null) {
|
||||||
|
const engineer = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: dto.engineerId,
|
||||||
|
role: Role.ENGINEER,
|
||||||
|
hospitalId,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!engineer) {
|
||||||
|
throw new BadRequestException('engineerId must be a valid local engineer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pressureByDeviceId = new Map(
|
||||||
|
devices.map((device) => [device.id, device.currentPressure] as const),
|
||||||
|
);
|
||||||
|
|
||||||
|
const task = await this.prisma.task.create({
|
||||||
|
data: {
|
||||||
|
status: TaskStatus.PENDING,
|
||||||
|
creatorId: actor.id,
|
||||||
|
engineerId: dto.engineerId ?? null,
|
||||||
|
hospitalId,
|
||||||
|
items: {
|
||||||
|
create: dto.items.map((item) => ({
|
||||||
|
deviceId: item.deviceId,
|
||||||
|
oldPressure: pressureByDeviceId.get(item.deviceId) ?? 0,
|
||||||
|
targetPressure: item.targetPressure,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.eventEmitter.emitAsync('task.published', {
|
||||||
|
taskId: task.id,
|
||||||
|
hospitalId: task.hospitalId,
|
||||||
|
actorId: actor.id,
|
||||||
|
status: task.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptTask(actor: ActorContext, dto: AcceptTaskDto) {
|
||||||
|
this.assertRole(actor, [Role.ENGINEER]);
|
||||||
|
const hospitalId = this.requireHospitalId(actor);
|
||||||
|
|
||||||
|
const task = await this.prisma.task.findFirst({
|
||||||
|
where: {
|
||||||
|
id: dto.taskId,
|
||||||
|
hospitalId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
hospitalId: true,
|
||||||
|
engineerId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new NotFoundException('Task not found in actor hospital');
|
||||||
|
}
|
||||||
|
if (task.status !== TaskStatus.PENDING) {
|
||||||
|
throw new ConflictException('Only pending task can be accepted');
|
||||||
|
}
|
||||||
|
if (task.engineerId != null && task.engineerId !== actor.id) {
|
||||||
|
throw new ForbiddenException('Task already assigned to another engineer');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTask = await this.prisma.task.update({
|
||||||
|
where: { id: task.id },
|
||||||
|
data: {
|
||||||
|
status: TaskStatus.ACCEPTED,
|
||||||
|
engineerId: actor.id,
|
||||||
|
},
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.eventEmitter.emitAsync('task.accepted', {
|
||||||
|
taskId: updatedTask.id,
|
||||||
|
hospitalId: updatedTask.hospitalId,
|
||||||
|
actorId: actor.id,
|
||||||
|
status: updatedTask.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeTask(actor: ActorContext, dto: CompleteTaskDto) {
|
||||||
|
this.assertRole(actor, [Role.ENGINEER]);
|
||||||
|
const hospitalId = this.requireHospitalId(actor);
|
||||||
|
|
||||||
|
const task = await this.prisma.task.findFirst({
|
||||||
|
where: {
|
||||||
|
id: dto.taskId,
|
||||||
|
hospitalId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
items: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new NotFoundException('Task not found in actor hospital');
|
||||||
|
}
|
||||||
|
if (task.status !== TaskStatus.ACCEPTED) {
|
||||||
|
throw new ConflictException('Only accepted task can be completed');
|
||||||
|
}
|
||||||
|
if (task.engineerId !== actor.id) {
|
||||||
|
throw new ForbiddenException('Only the assigned engineer can complete task');
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedTask = await this.prisma.$transaction(async (tx) => {
|
||||||
|
const nextTask = await tx.task.update({
|
||||||
|
where: { id: task.id },
|
||||||
|
data: { status: TaskStatus.COMPLETED },
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
task.items.map((item) =>
|
||||||
|
tx.device.update({
|
||||||
|
where: { id: item.deviceId },
|
||||||
|
data: { currentPressure: item.targetPressure },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return nextTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.eventEmitter.emitAsync('task.completed', {
|
||||||
|
taskId: completedTask.id,
|
||||||
|
hospitalId: completedTask.hospitalId,
|
||||||
|
actorId: actor.id,
|
||||||
|
status: completedTask.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return completedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelTask(actor: ActorContext, dto: CancelTaskDto) {
|
||||||
|
this.assertRole(actor, [Role.DOCTOR]);
|
||||||
|
const hospitalId = this.requireHospitalId(actor);
|
||||||
|
|
||||||
|
const task = await this.prisma.task.findFirst({
|
||||||
|
where: {
|
||||||
|
id: dto.taskId,
|
||||||
|
hospitalId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
creatorId: true,
|
||||||
|
hospitalId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new NotFoundException('Task not found in actor hospital');
|
||||||
|
}
|
||||||
|
if (task.creatorId !== actor.id) {
|
||||||
|
throw new ForbiddenException('Only task creator can cancel task');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
task.status !== TaskStatus.PENDING &&
|
||||||
|
task.status !== TaskStatus.ACCEPTED
|
||||||
|
) {
|
||||||
|
throw new ConflictException('Only pending or accepted task can be cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelledTask = await this.prisma.task.update({
|
||||||
|
where: { id: task.id },
|
||||||
|
data: { status: TaskStatus.CANCELLED },
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.eventEmitter.emitAsync('task.cancelled', {
|
||||||
|
taskId: cancelledTask.id,
|
||||||
|
hospitalId: cancelledTask.hospitalId,
|
||||||
|
actorId: actor.id,
|
||||||
|
status: cancelledTask.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return cancelledTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertRole(actor: ActorContext, allowedRoles: Role[]) {
|
||||||
|
if (!allowedRoles.includes(actor.role)) {
|
||||||
|
throw new ForbiddenException('Actor role is not allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private requireHospitalId(actor: ActorContext): number {
|
||||||
|
if (!actor.hospitalId) {
|
||||||
|
throw new BadRequestException('Actor hospitalId is required');
|
||||||
|
}
|
||||||
|
return actor.hospitalId;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/tasks/tasks.module.ts
Normal file
12
src/tasks/tasks.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BTasksController } from './b-tasks/b-tasks.controller.js';
|
||||||
|
import { TaskService } from './task.service.js';
|
||||||
|
import { AccessTokenGuard } from '../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../auth/roles.guard.js';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [BTasksController],
|
||||||
|
providers: [TaskService, AccessTokenGuard, RolesGuard],
|
||||||
|
exports: [TaskService],
|
||||||
|
})
|
||||||
|
export class TasksModule {}
|
||||||
25
src/users/b-users/b-users.controller.ts
Normal file
25
src/users/b-users/b-users.controller.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common';
|
||||||
|
import type { ActorContext } from '../../common/actor-context.js';
|
||||||
|
import { CurrentActor } from '../../auth/current-actor.decorator.js';
|
||||||
|
import { AccessTokenGuard } from '../../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../../auth/roles.guard.js';
|
||||||
|
import { Roles } from '../../auth/roles.decorator.js';
|
||||||
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
import { UsersService } from '../users.service.js';
|
||||||
|
import { AssignEngineerHospitalDto } from '../dto/assign-engineer-hospital.dto.js';
|
||||||
|
|
||||||
|
@Controller('b/users')
|
||||||
|
@UseGuards(AccessTokenGuard, RolesGuard)
|
||||||
|
export class BUsersController {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
@Patch(':id/assign-engineer-hospital')
|
||||||
|
@Roles(Role.SYSTEM_ADMIN)
|
||||||
|
assignEngineerHospital(
|
||||||
|
@CurrentActor() actor: ActorContext,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: AssignEngineerHospitalDto,
|
||||||
|
) {
|
||||||
|
return this.usersService.assignEngineerHospital(actor, Number(id), dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/users/dto/assign-engineer-hospital.dto.ts
Normal file
3
src/users/dto/assign-engineer-hospital.dto.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export class AssignEngineerHospitalDto {
|
||||||
|
hospitalId!: number;
|
||||||
|
}
|
||||||
@ -1 +1,12 @@
|
|||||||
export class CreateUserDto {}
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
name!: string;
|
||||||
|
phone!: string;
|
||||||
|
password?: string;
|
||||||
|
openId?: string;
|
||||||
|
role!: Role;
|
||||||
|
hospitalId?: number;
|
||||||
|
departmentId?: number;
|
||||||
|
groupId?: number;
|
||||||
|
}
|
||||||
|
|||||||
8
src/users/dto/login.dto.ts
Normal file
8
src/users/dto/login.dto.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
phone!: string;
|
||||||
|
password!: string;
|
||||||
|
role!: Role;
|
||||||
|
hospitalId?: number;
|
||||||
|
}
|
||||||
13
src/users/dto/register-user.dto.ts
Normal file
13
src/users/dto/register-user.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Role } from '../../generated/prisma/enums.js';
|
||||||
|
|
||||||
|
export class RegisterUserDto {
|
||||||
|
name!: string;
|
||||||
|
phone!: string;
|
||||||
|
password!: string;
|
||||||
|
role!: Role;
|
||||||
|
openId?: string;
|
||||||
|
hospitalId?: number;
|
||||||
|
departmentId?: number;
|
||||||
|
groupId?: number;
|
||||||
|
systemAdminBootstrapKey?: string;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreateUserDto } from './create-user.dto';
|
import { CreateUserDto } from './create-user.dto.js';
|
||||||
|
|
||||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||||
|
|||||||
@ -1,33 +1,52 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
import {
|
||||||
import { UsersService } from './users.service';
|
UseGuards,
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
Controller,
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AccessTokenGuard } from '../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../auth/roles.guard.js';
|
||||||
|
import { Roles } from '../auth/roles.decorator.js';
|
||||||
|
import { Role } from '../generated/prisma/enums.js';
|
||||||
|
import { UsersService } from './users.service.js';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto.js';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto.js';
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
|
@UseGuards(AccessTokenGuard, RolesGuard)
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private readonly usersService: UsersService) {}
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||||
create(@Body() createUserDto: CreateUserDto) {
|
create(@Body() createUserDto: CreateUserDto) {
|
||||||
return this.usersService.create(createUserDto);
|
return this.usersService.create(createUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.usersService.findAll();
|
return this.usersService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.usersService.findOne(+id);
|
return this.usersService.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
|
@Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN)
|
||||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||||
return this.usersService.update(+id, updateUserDto);
|
return this.usersService.update(+id, updateUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@Roles(Role.SYSTEM_ADMIN)
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.usersService.remove(+id);
|
return this.usersService.remove(+id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service.js';
|
||||||
import { UsersController } from './users.controller';
|
import { UsersController } from './users.controller.js';
|
||||||
|
import { BUsersController } from './b-users/b-users.controller.js';
|
||||||
|
import { AccessTokenGuard } from '../auth/access-token.guard.js';
|
||||||
|
import { RolesGuard } from '../auth/roles.guard.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [UsersController],
|
controllers: [UsersController, BUsersController],
|
||||||
providers: [UsersService],
|
providers: [UsersService, AccessTokenGuard, RolesGuard],
|
||||||
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|||||||
@ -1,26 +1,537 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import {
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
BadRequestException,
|
||||||
import { UpdateUserDto } from './dto/update-user.dto';
|
ConflictException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { compare, hash } from 'bcrypt';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto.js';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto.js';
|
||||||
|
import { Role } from '../generated/prisma/enums.js';
|
||||||
|
import { PrismaService } from '../prisma.service.js';
|
||||||
|
import { ActorContext } from '../common/actor-context.js';
|
||||||
|
import { AssignEngineerHospitalDto } from './dto/assign-engineer-hospital.dto.js';
|
||||||
|
import { RegisterUserDto } from './dto/register-user.dto.js';
|
||||||
|
import { LoginDto } from './dto/login.dto.js';
|
||||||
|
|
||||||
|
const SAFE_USER_SELECT = {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
phone: true,
|
||||||
|
openId: true,
|
||||||
|
role: true,
|
||||||
|
hospitalId: true,
|
||||||
|
departmentId: true,
|
||||||
|
groupId: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
create(createUserDto: CreateUserDto) {
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
return 'This action adds a new user';
|
|
||||||
|
async register(dto: RegisterUserDto) {
|
||||||
|
const role = this.normalizeRole(dto.role);
|
||||||
|
const name = this.normalizeRequiredString(dto.name, 'name');
|
||||||
|
const phone = this.normalizePhone(dto.phone);
|
||||||
|
const password = this.normalizePassword(dto.password);
|
||||||
|
const openId = this.normalizeOptionalString(dto.openId);
|
||||||
|
const hospitalId = this.normalizeOptionalInt(dto.hospitalId, 'hospitalId');
|
||||||
|
const departmentId = this.normalizeOptionalInt(
|
||||||
|
dto.departmentId,
|
||||||
|
'departmentId',
|
||||||
|
);
|
||||||
|
const groupId = this.normalizeOptionalInt(dto.groupId, 'groupId');
|
||||||
|
|
||||||
|
this.assertSystemAdminBootstrapKey(role, dto.systemAdminBootstrapKey);
|
||||||
|
await this.assertOrganizationScope(role, hospitalId, departmentId, groupId);
|
||||||
|
await this.assertOpenIdUnique(openId);
|
||||||
|
await this.assertPhoneRoleScopeUnique(phone, role, hospitalId);
|
||||||
|
|
||||||
|
const passwordHash = await hash(password, 12);
|
||||||
|
|
||||||
|
return this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
passwordHash,
|
||||||
|
openId,
|
||||||
|
role,
|
||||||
|
hospitalId,
|
||||||
|
departmentId,
|
||||||
|
groupId,
|
||||||
|
},
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
async login(dto: LoginDto) {
|
||||||
return `This action returns all users`;
|
const role = this.normalizeRole(dto.role);
|
||||||
|
const phone = this.normalizePhone(dto.phone);
|
||||||
|
const password = this.normalizePassword(dto.password);
|
||||||
|
const hospitalId = this.normalizeOptionalInt(dto.hospitalId, 'hospitalId');
|
||||||
|
|
||||||
|
const users = await this.prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
phone,
|
||||||
|
role,
|
||||||
|
...(hospitalId != null ? { hospitalId } : {}),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...SAFE_USER_SELECT,
|
||||||
|
passwordHash: true,
|
||||||
|
},
|
||||||
|
take: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
throw new UnauthorizedException('Invalid phone/role/password');
|
||||||
|
}
|
||||||
|
if (users.length > 1 && hospitalId == null) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Multiple accounts found. Please specify hospitalId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = users[0];
|
||||||
|
if (!user?.passwordHash) {
|
||||||
|
throw new UnauthorizedException('Password login is not enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = await compare(password, user.passwordHash);
|
||||||
|
if (!matched) {
|
||||||
|
throw new UnauthorizedException('Invalid phone/role/password');
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor: ActorContext = {
|
||||||
|
id: user.id,
|
||||||
|
role: user.role,
|
||||||
|
hospitalId: user.hospitalId,
|
||||||
|
departmentId: user.departmentId,
|
||||||
|
groupId: user.groupId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
accessToken: this.signAccessToken(actor),
|
||||||
|
actor,
|
||||||
|
user: this.toSafeUser(user),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number) {
|
async me(actor: ActorContext) {
|
||||||
return `This action returns a #${id} user`;
|
return this.findOne(actor.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, updateUserDto: UpdateUserDto) {
|
async create(createUserDto: CreateUserDto) {
|
||||||
return `This action updates a #${id} user`;
|
const role = this.normalizeRole(createUserDto.role);
|
||||||
|
const name = this.normalizeRequiredString(createUserDto.name, 'name');
|
||||||
|
const phone = this.normalizePhone(createUserDto.phone);
|
||||||
|
const password = createUserDto.password
|
||||||
|
? this.normalizePassword(createUserDto.password)
|
||||||
|
: null;
|
||||||
|
const openId = this.normalizeOptionalString(createUserDto.openId);
|
||||||
|
const hospitalId = this.normalizeOptionalInt(
|
||||||
|
createUserDto.hospitalId,
|
||||||
|
'hospitalId',
|
||||||
|
);
|
||||||
|
const departmentId = this.normalizeOptionalInt(
|
||||||
|
createUserDto.departmentId,
|
||||||
|
'departmentId',
|
||||||
|
);
|
||||||
|
const groupId = this.normalizeOptionalInt(createUserDto.groupId, 'groupId');
|
||||||
|
|
||||||
|
await this.assertOrganizationScope(role, hospitalId, departmentId, groupId);
|
||||||
|
await this.assertOpenIdUnique(openId);
|
||||||
|
await this.assertPhoneRoleScopeUnique(phone, role, hospitalId);
|
||||||
|
|
||||||
|
return this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
passwordHash: password ? await hash(password, 12) : null,
|
||||||
|
openId,
|
||||||
|
role,
|
||||||
|
hospitalId,
|
||||||
|
departmentId,
|
||||||
|
groupId,
|
||||||
|
},
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: number) {
|
async findAll() {
|
||||||
return `This action removes a #${id} user`;
|
return this.prisma.user.findMany({
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: number) {
|
||||||
|
const userId = this.normalizeRequiredInt(id, 'id');
|
||||||
|
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, updateUserDto: UpdateUserDto) {
|
||||||
|
const userId = this.normalizeRequiredInt(id, 'id');
|
||||||
|
const current = await this.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
...SAFE_USER_SELECT,
|
||||||
|
passwordHash: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!current) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRole =
|
||||||
|
updateUserDto.role != null ? this.normalizeRole(updateUserDto.role) : current.role;
|
||||||
|
const nextHospitalId =
|
||||||
|
updateUserDto.hospitalId !== undefined
|
||||||
|
? this.normalizeOptionalInt(updateUserDto.hospitalId, 'hospitalId')
|
||||||
|
: current.hospitalId;
|
||||||
|
const nextDepartmentId =
|
||||||
|
updateUserDto.departmentId !== undefined
|
||||||
|
? this.normalizeOptionalInt(updateUserDto.departmentId, 'departmentId')
|
||||||
|
: current.departmentId;
|
||||||
|
const nextGroupId =
|
||||||
|
updateUserDto.groupId !== undefined
|
||||||
|
? this.normalizeOptionalInt(updateUserDto.groupId, 'groupId')
|
||||||
|
: current.groupId;
|
||||||
|
|
||||||
|
await this.assertOrganizationScope(
|
||||||
|
nextRole,
|
||||||
|
nextHospitalId,
|
||||||
|
nextDepartmentId,
|
||||||
|
nextGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextOpenId =
|
||||||
|
updateUserDto.openId !== undefined
|
||||||
|
? this.normalizeOptionalString(updateUserDto.openId)
|
||||||
|
: current.openId;
|
||||||
|
await this.assertOpenIdUnique(nextOpenId, userId);
|
||||||
|
const nextPhone =
|
||||||
|
updateUserDto.phone !== undefined
|
||||||
|
? this.normalizePhone(updateUserDto.phone)
|
||||||
|
: current.phone;
|
||||||
|
await this.assertPhoneRoleScopeUnique(
|
||||||
|
nextPhone,
|
||||||
|
nextRole,
|
||||||
|
nextHospitalId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
if (updateUserDto.name !== undefined) {
|
||||||
|
data.name = this.normalizeRequiredString(updateUserDto.name, 'name');
|
||||||
|
}
|
||||||
|
if (updateUserDto.phone !== undefined) {
|
||||||
|
data.phone = nextPhone;
|
||||||
|
}
|
||||||
|
if (updateUserDto.role !== undefined) {
|
||||||
|
data.role = nextRole;
|
||||||
|
}
|
||||||
|
if (updateUserDto.hospitalId !== undefined) {
|
||||||
|
data.hospitalId = nextHospitalId;
|
||||||
|
}
|
||||||
|
if (updateUserDto.departmentId !== undefined) {
|
||||||
|
data.departmentId = nextDepartmentId;
|
||||||
|
}
|
||||||
|
if (updateUserDto.groupId !== undefined) {
|
||||||
|
data.groupId = nextGroupId;
|
||||||
|
}
|
||||||
|
if (updateUserDto.openId !== undefined) {
|
||||||
|
data.openId = nextOpenId;
|
||||||
|
}
|
||||||
|
if (updateUserDto.password) {
|
||||||
|
data.passwordHash = await hash(
|
||||||
|
this.normalizePassword(updateUserDto.password),
|
||||||
|
12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data,
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number) {
|
||||||
|
const userId = this.normalizeRequiredInt(id, 'id');
|
||||||
|
await this.findOne(userId);
|
||||||
|
|
||||||
|
return this.prisma.user.delete({
|
||||||
|
where: { id: userId },
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignEngineerHospital(
|
||||||
|
actor: ActorContext,
|
||||||
|
targetUserId: number,
|
||||||
|
dto: AssignEngineerHospitalDto,
|
||||||
|
) {
|
||||||
|
if (actor.role !== Role.SYSTEM_ADMIN) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Only SYSTEM_ADMIN can bind engineer to hospital',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(dto.hospitalId)) {
|
||||||
|
throw new BadRequestException('hospitalId must be an integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hospital = await this.prisma.hospital.findUnique({
|
||||||
|
where: { id: dto.hospitalId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!hospital) {
|
||||||
|
throw new NotFoundException('Hospital not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id: targetUserId },
|
||||||
|
select: { id: true, role: true },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
if (user.role !== Role.ENGINEER) {
|
||||||
|
throw new BadRequestException('Target user is not ENGINEER');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id: targetUserId },
|
||||||
|
data: {
|
||||||
|
hospitalId: dto.hospitalId,
|
||||||
|
departmentId: null,
|
||||||
|
groupId: null,
|
||||||
|
},
|
||||||
|
select: SAFE_USER_SELECT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private toSafeUser(user: { passwordHash?: string | null } & Record<string, unknown>) {
|
||||||
|
const { passwordHash, ...safe } = user;
|
||||||
|
return safe;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertSystemAdminBootstrapKey(
|
||||||
|
role: Role,
|
||||||
|
providedBootstrapKey?: string,
|
||||||
|
) {
|
||||||
|
if (role !== Role.SYSTEM_ADMIN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedBootstrapKey = process.env.SYSTEM_ADMIN_BOOTSTRAP_KEY;
|
||||||
|
if (!expectedBootstrapKey) {
|
||||||
|
throw new ForbiddenException('SYSTEM_ADMIN registration is disabled');
|
||||||
|
}
|
||||||
|
if (providedBootstrapKey !== expectedBootstrapKey) {
|
||||||
|
throw new ForbiddenException('Invalid system admin bootstrap key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertPhoneRoleScopeUnique(
|
||||||
|
phone: string,
|
||||||
|
role: Role,
|
||||||
|
hospitalId: number | null,
|
||||||
|
selfId?: number,
|
||||||
|
) {
|
||||||
|
const exists = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
phone,
|
||||||
|
role,
|
||||||
|
hospitalId,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (exists && exists.id !== selfId) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'User with same phone/role/hospital already exists',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertOpenIdUnique(openId: string | null, selfId?: number) {
|
||||||
|
if (!openId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.prisma.user.findUnique({
|
||||||
|
where: { openId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (exists && exists.id !== selfId) {
|
||||||
|
throw new ConflictException('openId already registered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertOrganizationScope(
|
||||||
|
role: Role,
|
||||||
|
hospitalId: number | null,
|
||||||
|
departmentId: number | null,
|
||||||
|
groupId: number | null,
|
||||||
|
) {
|
||||||
|
if (role === Role.SYSTEM_ADMIN) {
|
||||||
|
if (hospitalId || departmentId || groupId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'SYSTEM_ADMIN must not bind hospital/department/group',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hospitalId) {
|
||||||
|
throw new BadRequestException('hospitalId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hospital = await this.prisma.hospital.findUnique({
|
||||||
|
where: { id: hospitalId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!hospital) {
|
||||||
|
throw new BadRequestException('hospitalId does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsDepartment =
|
||||||
|
role === Role.DIRECTOR || role === Role.LEADER || role === Role.DOCTOR;
|
||||||
|
if (needsDepartment && !departmentId) {
|
||||||
|
throw new BadRequestException('departmentId is required for role');
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsGroup = role === Role.LEADER || role === Role.DOCTOR;
|
||||||
|
if (needsGroup && !groupId) {
|
||||||
|
throw new BadRequestException('groupId is required for role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === Role.ENGINEER && (departmentId || groupId)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'ENGINEER should not bind departmentId/groupId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departmentId) {
|
||||||
|
const department = await this.prisma.department.findUnique({
|
||||||
|
where: { id: departmentId },
|
||||||
|
select: { id: true, hospitalId: true },
|
||||||
|
});
|
||||||
|
if (!department || department.hospitalId !== hospitalId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'departmentId does not belong to hospitalId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
if (!departmentId) {
|
||||||
|
throw new BadRequestException('groupId requires departmentId');
|
||||||
|
}
|
||||||
|
const group = await this.prisma.group.findUnique({
|
||||||
|
where: { id: groupId },
|
||||||
|
select: { id: true, departmentId: true },
|
||||||
|
});
|
||||||
|
if (!group || group.departmentId !== departmentId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'groupId does not belong to departmentId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeRequiredInt(value: unknown, fieldName: string): number {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isInteger(parsed)) {
|
||||||
|
throw new BadRequestException(`${fieldName} must be an integer`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeOptionalInt(
|
||||||
|
value: unknown,
|
||||||
|
fieldName: string,
|
||||||
|
): number | null {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.normalizeRequiredInt(value, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeRequiredString(value: unknown, fieldName: string): string {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new BadRequestException(`${fieldName} must be a string`);
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new BadRequestException(`${fieldName} is required`);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeOptionalString(value: unknown): string | null {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new BadRequestException('openId must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePhone(phone: unknown): string {
|
||||||
|
const normalized = this.normalizeRequiredString(phone, 'phone');
|
||||||
|
if (!/^1\d{10}$/.test(normalized)) {
|
||||||
|
throw new BadRequestException('phone must be a valid CN mobile number');
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePassword(password: unknown): string {
|
||||||
|
const normalized = this.normalizeRequiredString(password, 'password');
|
||||||
|
if (normalized.length < 8) {
|
||||||
|
throw new BadRequestException('password must be at least 8 characters');
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeRole(role: unknown): Role {
|
||||||
|
if (typeof role !== 'string') {
|
||||||
|
throw new BadRequestException('role must be a string enum');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.values(Role).includes(role as Role)) {
|
||||||
|
throw new BadRequestException(`invalid role: ${role}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return role as Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
private signAccessToken(actor: ActorContext): string {
|
||||||
|
const secret = process.env.AUTH_TOKEN_SECRET;
|
||||||
|
if (!secret) {
|
||||||
|
throw new UnauthorizedException('AUTH_TOKEN_SECRET is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.sign(actor, secret, {
|
||||||
|
algorithm: 'HS256',
|
||||||
|
expiresIn: '7d',
|
||||||
|
issuer: 'tyt-api-nest',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user