feat: Authentication, Register users, Create Rekammedis + logs, Get All Users, Obat, Tindakan, Rekam Medis

This commit is contained in:
yosaphatprs 2025-10-27 13:41:51 +07:00
parent fb09ff57d5
commit d73a44cceb
60 changed files with 1218 additions and 142 deletions

View File

@ -12,8 +12,13 @@
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@prisma/client": "^6.17.1", "@prisma/client": "^6.17.1",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
@ -24,6 +29,7 @@
"@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/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
@ -2303,14 +2309,14 @@
} }
}, },
"node_modules/@nestjs/common": { "node_modules/@nestjs/common": {
"version": "11.1.6", "version": "11.1.7",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz",
"integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"file-type": "21.0.0", "file-type": "21.0.0",
"iterare": "1.2.1", "iterare": "1.2.1",
"load-esm": "1.0.2", "load-esm": "1.0.3",
"tslib": "2.8.1", "tslib": "2.8.1",
"uid": "2.0.2" "uid": "2.0.2"
}, },
@ -2349,16 +2355,16 @@
} }
}, },
"node_modules/@nestjs/core": { "node_modules/@nestjs/core": {
"version": "11.1.6", "version": "11.1.7",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz",
"integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nuxt/opencollective": "0.4.1", "@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1", "fast-safe-stringify": "2.1.1",
"iterare": "1.2.1", "iterare": "1.2.1",
"path-to-regexp": "8.2.0", "path-to-regexp": "8.3.0",
"tslib": "2.8.1", "tslib": "2.8.1",
"uid": "2.0.2" "uid": "2.0.2"
}, },
@ -2389,16 +2395,49 @@
} }
} }
}, },
"node_modules/@nestjs/jwt": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz",
"integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "9.0.10",
"jsonwebtoken": "9.0.2"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
"integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
"class-validator": "^0.13.0 || ^0.14.0",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/platform-express": { "node_modules/@nestjs/platform-express": {
"version": "11.1.6", "version": "11.1.7",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz",
"integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", "integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cors": "2.8.5", "cors": "2.8.5",
"express": "5.1.0", "express": "5.1.0",
"multer": "2.0.2", "multer": "2.0.2",
"path-to-regexp": "8.2.0", "path-to-regexp": "8.3.0",
"tslib": "2.8.1" "tslib": "2.8.1"
}, },
"funding": { "funding": {
@ -2509,9 +2548,9 @@
} }
}, },
"node_modules/@nestjs/testing": { "node_modules/@nestjs/testing": {
"version": "11.1.6", "version": "11.1.7",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.7.tgz",
"integrity": "sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==", "integrity": "sha512-QbtrgSlc3QVo6RHNxTTlyhaiobLLy8kvhOlgWHsoXRknybuRs7vZg4k5mo3ye6pITGeT3CrWIRpZjUsh5Wps5Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2864,6 +2903,16 @@
"@babel/types": "^7.28.2" "@babel/types": "^7.28.2"
} }
}, },
"node_modules/@types/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.6", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@ -3008,6 +3057,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/methods": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -3022,11 +3081,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.18.12", "version": "22.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -3110,6 +3174,12 @@
"@types/superagent": "^8.1.0" "@types/superagent": "^8.1.0"
} }
}, },
"node_modules/@types/validator": {
"version": "13.15.3",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
"integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==",
"license": "MIT"
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.33", "version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@ -4237,6 +4307,20 @@
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
} }
}, },
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/bl": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -4387,6 +4471,12 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -4618,6 +4708,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
"license": "MIT"
},
"node_modules/class-validator": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
"license": "MIT",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.11.1",
"validator": "^13.9.0"
}
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -5185,6 +5292,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -7641,6 +7757,49 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"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.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -7675,6 +7834,12 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/libphonenumber-js": {
"version": "1.12.24",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.24.tgz",
"integrity": "sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==",
"license": "MIT"
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -7683,9 +7848,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/load-esm": { "node_modules/load-esm": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz",
"integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -7737,6 +7902,42 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -7751,6 +7952,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@ -8129,6 +8336,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-emoji": { "node_modules/node-emoji": {
"version": "1.11.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@ -8146,6 +8362,17 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-int64": { "node_modules/node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@ -8642,12 +8869,13 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.2.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT", "license": "MIT",
"engines": { "funding": {
"node": ">=16" "type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/path-type": { "node_modules/path-type": {
@ -9233,7 +9461,6 @@
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -10386,7 +10613,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": { "node_modules/universalify": {
@ -10512,6 +10738,15 @@
"node": ">=10.12.0" "node": ">=10.12.0"
} }
}, },
"node_modules/validator": {
"version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -23,8 +23,13 @@
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.1",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@prisma/client": "^6.17.1", "@prisma/client": "^6.17.1",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
@ -35,6 +40,7 @@
"@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/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",

View File

@ -9,19 +9,20 @@ datasource db {
} }
model blockchain_log_queue { model blockchain_log_queue {
id BigInt @id @default(autoincrement()) id BigInt @id @default(autoincrement())
status String @default("PENDING") @db.VarChar(20) status String @default("PENDING") @db.VarChar(20)
event String @db.VarChar(50) event String @db.VarChar(50)
user_id BigInt? user_id BigInt
created_at DateTime? @default(now()) @db.Timestamptz(6) created_at DateTime? @default(now()) @db.Timestamptz(6)
payload Json? payload Json
processed_at DateTime? @db.Timestamptz(6) processed_at DateTime? @db.Timestamptz(6)
users users? @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "fk_log_user") transactionid String? @db.VarChar(64)
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_log_user")
} }
model pemberian_obat { model pemberian_obat {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
id_visit Int id_visit String @db.VarChar(25)
obat String @db.VarChar(100) obat String @db.VarChar(100)
jumlah_obat Int jumlah_obat Int
aturan_pakai String? aturan_pakai String?
@ -30,7 +31,7 @@ model pemberian_obat {
model pemberian_tindakan { model pemberian_tindakan {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
id_visit Int id_visit String @db.VarChar(25)
tindakan String @db.VarChar(100) tindakan String @db.VarChar(100)
kategori_tindakan String? @db.VarChar(50) kategori_tindakan String? @db.VarChar(50)
kelompok_tindakan String? @db.VarChar(50) kelompok_tindakan String? @db.VarChar(50)
@ -38,7 +39,7 @@ model pemberian_tindakan {
} }
model rekam_medis { model rekam_medis {
id_visit Int @id @default(autoincrement()) id_visit String @id @db.VarChar(25)
waktu_visit DateTime @db.Timestamp(6) waktu_visit DateTime @db.Timestamp(6)
no_rm String @db.VarChar(20) no_rm String @db.VarChar(20)
nama_pasien String @db.VarChar(100) nama_pasien String @db.VarChar(100)
@ -55,8 +56,8 @@ model rekam_medis {
nadi Int? nadi Int?
suhu Decimal? @db.Decimal(4, 1) suhu Decimal? @db.Decimal(4, 1)
nafas Int? nafas Int?
tinggi_badan Decimal? @db.Decimal(5, 2) tinggi_badan Decimal? @db.Decimal(10, 5)
berat_badan Decimal? @db.Decimal(5, 2) berat_badan Decimal? @db.Decimal(10, 5)
jenis_kasus String? @db.VarChar(50) jenis_kasus String? @db.VarChar(50)
tindak_lanjut String? tindak_lanjut String?
pemberian_obat pemberian_obat[] pemberian_obat pemberian_obat[]

View File

@ -1,25 +1,28 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { UserModule } from './user/user.module'; import { UserModule } from './modules/user/user.module';
import { LogModule } from './log/log.module'; import { LogModule } from './modules/log/log.module';
import { RekamMedisModule } from './rekammedis/rekammedis.module'; import { RekamMedisModule } from './modules/rekammedis/rekammedis.module';
import { ObatModule } from './obat/obat.module'; import { ObatModule } from './modules/obat/obat.module';
import { TindakandokterModule } from './tindakandokter/tindakandokter.module'; import { TindakanDokterModule } from './modules/tindakandokter/tindakandokter.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module'; import { PrismaModule } from './modules/prisma/prisma.module';
import { AuthModule } from './modules/auth/auth.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
}), }),
AuthModule,
UserModule, UserModule,
TindakanDokterModule,
LogModule, LogModule,
RekamMedisModule, RekamMedisModule,
ObatModule, ObatModule,
TindakandokterModule,
PrismaModule, PrismaModule,
AuthModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@ -2,22 +2,25 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { ValidationPipe } from '@nestjs/common';
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
app.setGlobalPrefix('api/');
app.enableCors();
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
app.use(cookieParser(configService.get<string>('COOKIE_SECRET'))); app.use(cookieParser(configService.get<string>('COOKIE_SECRET')));
await app.listen(configService.get<number>('PORT') ?? 1323); await app.listen(configService.get<number>('PORT') ?? 1323);
} }
bootstrap(); bootstrap();
// Module
// APP
// - Users
// -- Auth
// -- Profiles
// -- Register
// - Logs
// - RekamMedis
// - Obat
// - TindakanDokter

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,35 @@
import {
Body,
Controller,
Header,
HttpCode,
Post,
UseGuards,
} from '@nestjs/common';
import { CreateUserDto, CreateUserDtoResponse } from './dto/create-user.dto';
import { AuthDto, AuthDtoResponse, UserRole } from './dto/auth.dto';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('/auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('/register')
@Header('Content-Type', 'application/json')
@HttpCode(201)
@UseGuards(AuthGuard, RolesGuard)
@Roles(UserRole.Admin)
registerUser(@Body() data: CreateUserDto): Promise<CreateUserDtoResponse> {
return this.authService.registerUser(data);
}
@Post('/login')
@Header('Content-Type', 'application/json')
@HttpCode(200)
loginUser(@Body() data: AuthDto): Promise<AuthDtoResponse> {
return this.authService.signIn(data.username, data.password);
}
}

View File

@ -0,0 +1,9 @@
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from './auth.guard';
import { ConfigService } from '@nestjs/config';
describe('AuthGuard', () => {
it('should be defined', () => {
expect(new AuthGuard(new JwtService(), new ConfigService())).toBeDefined();
});
});

View File

@ -0,0 +1,38 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('JWT_SECRET'),
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers?.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PrismaModule } from '../prisma/prisma.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
@Module({
exports: [AuthService],
imports: [
PrismaModule,
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
}),
],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}

View File

@ -0,0 +1,74 @@
import { PrismaService } from '@api/modules/prisma/prisma.service';
import {
ConflictException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthDtoResponse, UserRole } from './dto/auth.dto';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
import { CreateUserDto, CreateUserDtoResponse } from './dto/create-user.dto';
import { ConfigService } from '@nestjs/config';
import { Prisma } from '@dist/generated/prisma';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async registerUser(data: CreateUserDto): Promise<CreateUserDtoResponse> {
const salt = this.configService.get<number>('BCRYPT_SALT') ?? 10;
const hashedPassword = await bcrypt.hash(data.password, salt);
try {
const userCreated = await this.prisma.users.create({
data: {
nama_lengkap: data.nama_lengkap,
username: data.username,
password_hash: hashedPassword,
role: data.role || 'user',
},
});
return {
id: userCreated.id,
nama_lengkap: userCreated.nama_lengkap,
username: userCreated.username,
role: userCreated.role as UserRole,
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new ConflictException('Username ini sudah terdaftar');
}
}
throw error;
}
}
async signIn(username: string, password: string): Promise<AuthDtoResponse> {
const user = await this.prisma.users.findUnique({
where: { username },
});
if (!user || !(await bcrypt.compare(password, user.password_hash))) {
throw new UnauthorizedException('Username atau password salah');
}
const token = await this.jwtService.signAsync({
sub: user.id,
username: user.username,
role: user.role,
});
return {
id: user.id,
username: user.username,
role: user.role as UserRole,
token,
};
}
}

View File

@ -0,0 +1,35 @@
import { IsString, IsNotEmpty, Length, IsEnum } from 'class-validator';
import { Expose, Transform } from 'class-transformer';
export enum UserRole {
Admin = 'admin',
User = 'user',
}
export class AuthDto {
@IsNotEmpty({ message: 'Username wajib diisi' })
@IsString({ message: 'Username harus berupa string' })
@Length(1, 100, { message: 'Username maksimal 100 karakter' })
username: string;
@IsNotEmpty({ message: 'Password wajib diisi' })
@IsString({ message: 'Password harus berupa string' })
@Length(6, undefined, { message: 'Password minimal 6 karakter' })
password: string;
}
export class AuthDtoResponse {
@Expose()
@Transform(({ value }: { value: bigint }) => value.toString())
id: bigint;
@Expose()
username: string;
@Expose()
@IsEnum(UserRole)
role: UserRole;
@Expose()
token: string;
}

View File

@ -0,0 +1,48 @@
import {
IsString,
IsNotEmpty,
Length,
IsOptional,
IsEnum,
} from 'class-validator';
import { Expose, Transform } from 'class-transformer';
import { UserRole } from './auth.dto';
export class CreateUserDto {
@IsNotEmpty({ message: 'Nama lengkap wajib diisi' })
@IsString({ message: 'Nama lengkap harus berupa string' })
@Length(1, 255, { message: 'Nama lengkap maksimal 255 karakter' })
nama_lengkap: string;
@IsNotEmpty({ message: 'Username wajib diisi' })
@IsString({ message: 'Username harus berupa string' })
@Length(1, 100, { message: 'Username maksimal 100 karakter' })
username: string;
@IsNotEmpty({ message: 'Password wajib diisi' })
@IsString({ message: 'Password harus berupa string' })
@Length(6, 100, {
message: 'Password minimal 6 karakter dan maksimal 100 karakter',
})
password: string;
@IsOptional()
@IsString({ message: 'Role harus berupa string' })
@IsEnum(UserRole, { message: 'Role harus "admin" atau "user"' })
role?: UserRole;
}
export class CreateUserDtoResponse {
@Expose()
@Transform(({ value }: { value: bigint }) => value.toString())
id: bigint;
@Expose()
nama_lengkap: string;
@Expose()
username: string;
@Expose()
role?: UserRole;
}

View File

@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
import { UserRole } from './dto/auth.dto';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);

View File

@ -0,0 +1,7 @@
import { RolesGuard } from './roles.guard';
describe('RolesGuard', () => {
it('should be defined', () => {
expect(new RolesGuard()).toBeDefined();
});
});

View File

@ -0,0 +1,39 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
import { UserRole } from './dto/auth.dto';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user?.role) {
throw new ForbiddenException('Insufficient permissions (no role)');
}
const hasRole = requiredRoles.some((role) => user.role === role);
if (hasRole) {
return true;
}
throw new ForbiddenException('You do not have the required role');
}
}

View File

@ -0,0 +1,40 @@
import { IsString, IsNotEmpty, Length, IsJSON, IsEnum } from 'class-validator';
export class CreateLogDto {
@IsNotEmpty({ message: 'Event wajib diisi' })
@IsString({ message: 'Event harus berupa string' })
@IsEnum(
[
'tindakan_dokter_created',
'obat_given',
'rekam_medis_created',
'tindakan_dokter_updated',
'obat_updated',
'rekam_medis_updated',
'tindakan_dokter_deleted',
'obat_deleted',
'rekam_medis_deleted',
],
{
message: 'Event tidak valid',
},
)
@Length(1, 100, { message: 'Event maksimal 100 karakter' })
event: string;
@IsNotEmpty({ message: 'Payload wajib diisi' })
@IsJSON({ message: 'Payload harus berupa JSON yang valid' })
payload: {
dokter_id: number;
visit_id: string;
tindakan?: string;
kategori_tindakan?: string;
kelompok_tindakan?: string;
obat?: string;
jumlah_obat?: number;
aturan_pakai?: string;
anamnese?: string;
jenis_kasus?: string;
tindak_lanjut?: string;
};
}

View File

@ -1,7 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { LogController } from './log.controller'; import { LogController } from './log.controller';
import { LogService } from './log.service';
@Module({ @Module({
controllers: [LogController] controllers: [LogController],
providers: [LogService]
}) })
export class LogModule {} export class LogModule {}

View File

@ -1,15 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { RegisterService } from './register.service'; import { LogService } from './log.service';
describe('RegisterService', () => { describe('LogService', () => {
let service: RegisterService; let service: LogService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [RegisterService], providers: [LogService],
}).compile(); }).compile();
service = module.get<RegisterService>(RegisterService); service = module.get<LogService>(LogService);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AuthService {} export class LogService {}

View File

@ -0,0 +1,26 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ObatService } from './obat.service';
@Controller('obat')
export class ObatController {
constructor(private readonly obatService: ObatService) {}
@Get('/')
async getAllObat(
@Query('take') take: number,
@Query('skip') skip: number,
@Query('page') page: number,
@Query('orderBy') orderBy: string,
@Query('obat') obat: string,
@Query('order') order: 'asc' | 'desc',
) {
return await this.obatService.getAllObat({
take,
skip,
page,
orderBy: orderBy ? { [orderBy]: order || 'asc' } : undefined,
obat,
order,
});
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ObatController } from './obat.controller';
import { ObatService } from './obat.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [ObatController],
providers: [ObatService],
})
export class ObatModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ObatService } from './obat.service';
describe('ObatService', () => {
let service: ObatService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ObatService],
}).compile();
service = module.get<ObatService>(ObatService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ObatService {
constructor(private prisma: PrismaService) {}
async getAllObat(params: {
take?: number;
skip?: number;
page?: number;
orderBy?: any;
obat?: string;
order?: 'asc' | 'desc';
}) {
const { skip, page, orderBy, order, obat } = params;
const take = params.take ? parseInt(params.take.toString()) : 10;
const skipValue = skip
? parseInt(skip.toString())
: page
? (parseInt(page.toString()) - 1) * take
: 0;
const results = await this.prisma.pemberian_obat.findMany({
skip: skipValue,
take: take,
where: {
obat: obat ? { contains: obat } : undefined,
},
orderBy: orderBy
? { [Object.keys(orderBy)[0]]: order || 'asc' }
: { id: 'asc' },
});
console.log('Fetched Obat:', results.length);
return results;
}
}

View File

@ -0,0 +1,10 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@dist/generated/prisma';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
console.log('PrismaService has connected to the database.');
}
}

View File

@ -0,0 +1,115 @@
import {
IsString,
IsNotEmpty,
IsOptional,
IsInt,
IsNumber,
IsDateString,
Min,
Max,
Length,
Matches,
IsIn,
} from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateRekamMedisDto {
@IsNotEmpty({ message: 'Nomor rekam medis (no_rm) wajib diisi' })
@IsString()
@Length(1, 20, { message: 'Nomor rekam medis maksimal 20 karakter' })
no_rm: string;
@IsNotEmpty({ message: 'Nama pasien wajib diisi' })
@IsString()
@Length(1, 100, { message: 'Nama pasien maksimal 100 karakter' })
nama_pasien: string;
@IsOptional()
@IsInt({ message: 'Umur harus berupa angka bulat' })
@Min(0, { message: 'Umur tidak boleh negatif' })
@Max(150, { message: 'Umur tidak valid' })
@Transform(({ value }) => (value ? parseInt(value) : null))
umur?: number;
@IsOptional()
@IsString()
@IsIn(['L', 'P', 'l', 'p'], {
message: 'Jenis kelamin harus "L" (Laki-laki) atau "P" (Perempuan)',
})
@Transform(({ value }) => value?.toUpperCase())
jenis_kelamin?: string;
@IsOptional()
@IsString()
@IsIn(['A', 'B', 'AB', 'O', '-'], {
message: 'Golongan darah harus A, B, AB, O, atau -',
})
@Length(1, 2)
gol_darah?: string;
@IsOptional()
@IsString()
@Length(1, 100)
pekerjaan?: string;
@IsOptional()
@IsString()
@Length(1, 100)
suku?: string;
@IsOptional()
@IsString()
kode_diagnosa?: string;
@IsOptional()
@IsString()
diagnosa?: string;
@IsOptional()
@IsString()
anamnese?: string;
@IsOptional()
@IsInt({ message: 'Tekanan darah sistolik harus berupa angka bulat' })
@Transform(({ value }) => (value ? parseInt(value) : null))
sistolik?: number;
@IsOptional()
@IsInt({ message: 'Tekanan darah diastolik harus berupa angka bulat' })
@Transform(({ value }) => (value ? parseInt(value) : null))
diastolik?: number;
@IsOptional()
@IsInt({ message: 'Nadi harus berupa angka bulat' })
@Transform(({ value }) => (value ? parseInt(value) : null))
nadi?: number;
@IsOptional()
@IsNumber({}, { message: 'Suhu harus berupa angka' })
@Transform(({ value }) => (value ? parseFloat(value) : null))
suhu?: number;
@IsOptional()
@IsInt({ message: 'Pernapasan harus berupa angka bulat' })
@Transform(({ value }) => (value ? parseInt(value) : null))
nafas?: number;
@IsOptional()
@IsNumber({}, { message: 'Tinggi badan harus berupa angka' })
@Transform(({ value }) => (value ? parseFloat(value) : null))
tinggi_badan?: number;
@IsOptional()
@IsNumber({}, { message: 'Berat badan harus berupa angka' })
@Transform(({ value }) => (value ? parseFloat(value) : null))
berat_badan?: number;
@IsOptional()
@IsString()
@Length(1, 50)
jenis_kasus?: string;
@IsOptional()
@IsString()
tindak_lanjut?: string;
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateRekamMedisDto } from './create-rekammedis.dto';
export class UpdateRekamMedisDto extends PartialType(CreateRekamMedisDto) {}

View File

@ -0,0 +1,46 @@
import {
Body,
Controller,
Get,
Header,
HttpCode,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { RekammedisService } from './rekammedis.service';
import { CreateRekamMedisDto } from './dto/create-rekammedis.dto';
import { AuthGuard } from '../auth/auth.guard';
@Controller('/rekammedis')
export class RekamMedisController {
constructor(private readonly rekammedisService: RekammedisService) {}
@Get('/')
@Header('Content-Type', 'application/json')
@HttpCode(200)
@UseGuards(AuthGuard)
async getAllRekamMedis(
@Query('take') take: number,
@Query('skip') skip: number,
@Query('page') page: number,
@Query('orderBy') orderBy: string,
@Query('no_rm') no_rm: string,
@Query('order') order: 'asc' | 'desc',
) {
return this.rekammedisService.getAllRekamMedis({
take,
skip,
page,
orderBy,
no_rm,
order,
});
}
@Post('/')
@Header('Content-Type', 'application/json')
async createRekamMedis(@Body() dto: CreateRekamMedisDto) {
return this.rekammedisService.createRekamMedis(dto);
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { RekamMedisController } from './rekammedis.controller';
import { RekammedisService } from './rekammedis.service';
import { PrismaModule } from '../prisma/prisma.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [PrismaModule, JwtModule],
controllers: [RekamMedisController],
providers: [RekammedisService],
})
export class RekamMedisModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RekammedisService } from '../rekammedis/rekammedis.service';
describe('RekammedisService', () => {
let service: RekammedisService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RekammedisService],
}).compile();
service = module.get<RekammedisService>(RekammedisService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,106 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma, rekam_medis } from '@dist/generated/prisma';
import { CreateRekamMedisDto } from './dto/create-rekammedis.dto';
import { CreateLogDto } from '../log/dto/create-log.dto';
@Injectable()
export class RekammedisService {
constructor(private prisma: PrismaService) {}
async getAllRekamMedis(params: {
take?: number;
skip?: number;
page?: number;
orderBy?: any;
no_rm?: string;
order?: 'asc' | 'desc';
}): Promise<rekam_medis[]> {
const { skip, page, orderBy, order, no_rm } = params;
const take = params.take ? parseInt(params.take.toString()) : 10;
const skipValue = skip
? parseInt(skip.toString())
: page
? (parseInt(page.toString()) - 1) * take
: 0;
const results = await this.prisma.rekam_medis.findMany({
skip: skipValue,
take: take,
where: {
no_rm: no_rm ? no_rm : undefined,
},
orderBy: orderBy
? { [orderBy]: order || 'asc' }
: { waktu_visit: order ? order : 'asc' },
});
console.log('Fetched Rekam Medis:', results.length);
return results;
}
async createRekamMedis(data: CreateRekamMedisDto) {
const latestId = await this.prisma.rekam_medis.findFirst({
orderBy: { waktu_visit: 'desc' },
});
let newId = '';
let xCounter = 0;
let rekamMedis: Prisma.rekam_medisCreateInput;
for (let i = (latestId?.id_visit?.length ?? 0) - 1; i >= 0; i--) {
if (latestId?.id_visit[i] === 'X') {
xCounter++;
} else {
newId = latestId?.id_visit?.substring(0, i + 1) || '';
break;
}
}
if (xCounter < 1) {
newId = (parseInt(latestId?.id_visit || '0', 10) + 1).toString();
} else {
newId = (parseInt(newId || '0', 10) + 1).toString();
}
rekamMedis = {
...data,
id_visit: newId,
waktu_visit: new Date(),
};
const logData: CreateLogDto = {
event: 'rekam_medis_created',
payload: {
dokter_id: 123,
visit_id: newId,
anamnese: data.anamnese,
jenis_kasus: data.jenis_kasus,
tindak_lanjut: data.tindak_lanjut,
},
};
try {
const newRekamMedis = await this.prisma.$transaction(async (tx) => {
const createdRekamMedis = await tx.rekam_medis.create({
data: rekamMedis,
});
await tx.blockchain_log_queue.create({
data: {
event: logData.event,
user_id: 9,
payload: logData.payload,
},
});
return createdRekamMedis;
});
return newRekamMedis;
} catch (error) {
console.error('Error creating Rekam Medis:', error);
throw error;
}
}
}

View File

@ -1,15 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { TindakandokterController } from './tindakandokter.controller'; import { TindakanDokterController } from './tindakandokter.controller';
describe('TindakandokterController', () => { describe('TindakanDokterController', () => {
let controller: TindakandokterController; let controller: TindakanDokterController;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [TindakandokterController], controllers: [TindakanDokterController],
}).compile(); }).compile();
controller = module.get<TindakandokterController>(TindakandokterController); controller = module.get<TindakanDokterController>(TindakanDokterController);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -0,0 +1,32 @@
import {
Controller,
Get,
Header,
HttpCode,
Param,
Query,
} from '@nestjs/common';
import { TindakanDokterService } from './tindakandokter.service';
@Controller('/tindakan')
export class TindakanDokterController {
constructor(private tindakanDokterService: TindakanDokterService) {}
@Get('/')
async getAllTindakanDokter(
@Query('take') take: number,
@Query('tindakan') tindakan: string,
@Query('skip') skip: number,
@Query('page') page: number,
@Query('orderBy') orderBy: string,
@Query('order') order: 'asc' | 'desc',
) {
return await this.tindakanDokterService.getAllTindakanDokter({
take,
tindakan,
skip,
page,
orderBy: orderBy ? { [orderBy]: order || 'asc' } : undefined,
});
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TindakanDokterController } from './tindakandokter.controller';
import { TindakanDokterService } from './tindakandokter.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [TindakanDokterController],
providers: [TindakanDokterService],
})
export class TindakanDokterModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TindakanDokterService } from './tindakandokter.service';
describe('TindakandokterService', () => {
let service: TindakanDokterService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TindakanDokterService],
}).compile();
service = module.get<TindakanDokterService>(TindakanDokterService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma } from '@dist/generated/prisma';
@Injectable()
export class TindakanDokterService {
constructor(private prisma: PrismaService) {}
async getAllTindakanDokter(params: {
skip?: number;
take?: number;
page?: number;
tindakan?: string;
orderBy?: Prisma.pemberian_tindakanOrderByWithRelationInput;
order?: 'asc' | 'desc';
}) {
const { skip, page, tindakan, orderBy, order } = params;
const take = params.take ? parseInt(params.take.toString()) : 10;
const skipValue = skip
? parseInt(skip.toString())
: page
? (parseInt(page.toString()) - 1) * take
: 0;
const results = await this.prisma.pemberian_tindakan.findMany({
skip: skipValue,
take: take,
where: {
tindakan: tindakan ? { contains: tindakan } : undefined,
},
orderBy: orderBy
? { [Object.keys(orderBy)[0]]: order || 'asc' }
: undefined,
});
return results;
}
}

View File

@ -1,4 +1,6 @@
import { import {
Body,
ClassSerializerInterceptor,
Controller, Controller,
Get, Get,
Header, Header,
@ -8,23 +10,16 @@ import {
Query, Query,
Req, Req,
Res, Res,
UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { users } from '@dist/generated/prisma';
@Controller('/api/users') @Controller('/users')
@UseInterceptors(ClassSerializerInterceptor)
export class UserController { export class UserController {
constructor(private service: UserService) {} constructor(private userService: UserService) {}
@Post('/register')
registerUser(): string {
return this.service.testService();
}
@Post('/login')
loginUser(): string {
return 'User logged in successfully';
}
@Get('/profile/:id') @Get('/profile/:id')
getUserProfile(@Param('id') id: string): string { getUserProfile(@Param('id') id: string): string {
@ -34,9 +29,8 @@ export class UserController {
@Get('/') @Get('/')
@Header('Content-Type', 'application/json') @Header('Content-Type', 'application/json')
@HttpCode(200) @HttpCode(200)
getAllUsers(): Record<string, string> { getAllUsers(): Promise<users[]> {
return { message: 'List of all users' }; return this.userService.getAllUsers();
// return 'List of all users';
} }
@Get('/set-cookie') @Get('/set-cookie')

View File

@ -1,12 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { AuthService } from './auth/auth.service';
import { RegisterService } from './register/register.service';
import { ProfileService } from './profile/profile.service'; import { ProfileService } from './profile/profile.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({ @Module({
imports: [PrismaModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService, AuthService, RegisterService, ProfileService], providers: [UserService, ProfileService],
}) })
export class UserModule {} export class UserModule {}

View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { users } from '@dist/generated/prisma';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async getAllUsers(): Promise<users[]> {
return this.prisma.users.findMany();
}
}

View File

@ -1,4 +0,0 @@
import { Controller } from '@nestjs/common';
@Controller('obat')
export class ObatController {}

View File

@ -1,7 +0,0 @@
import { Module } from '@nestjs/common';
import { ObatController } from './obat.controller';
@Module({
controllers: [ObatController]
})
export class ObatModule {}

View File

@ -1,10 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@dist/generated/prisma';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
super();
console.log('PrismaService initialized');
}
}

View File

@ -1,4 +0,0 @@
import { Controller } from '@nestjs/common';
@Controller('rekammedis')
export class RekamMedisController {}

View File

@ -1,7 +0,0 @@
import { Module } from '@nestjs/common';
import { RekamMedisController } from './rekammedis.controller';
@Module({
controllers: [RekamMedisController],
})
export class RekamMedisModule {}

View File

@ -1,4 +0,0 @@
import { Controller } from '@nestjs/common';
@Controller('tindakandokter')
export class TindakandokterController {}

View File

@ -1,7 +0,0 @@
import { Module } from '@nestjs/common';
import { TindakandokterController } from './tindakandokter.controller';
@Module({
controllers: [TindakandokterController]
})
export class TindakandokterModule {}

View File

@ -1,4 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class RegisterService {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
testService(): string {
return 'User service is working';
}
}