Merge branch 'dev'

This commit is contained in:
Methapon2001 2024-04-19 10:25:22 +07:00
commit 2b843d7e13
44 changed files with 4242 additions and 105 deletions

View file

@ -7,4 +7,15 @@ KC_SERVICE_ACCOUNT_SECRET=
APP_HOST=0.0.0.0 APP_HOST=0.0.0.0
APP_PORT=3000 APP_PORT=3000
MINIO_HOST=192.168.1.20
MINIO_PORT=9000
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_BUCKET=jws-dev
ELASTICSEARCH_PROTOCOL=http
ELASTICSEARCH_HOST=192.168.1.20
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_INDEX=jws-log-index
DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public

74
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,74 @@
name: release-test
run-name: release-test ${{ github.actor }}
on:
push:
tags:
- "version-[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
env:
REGISTRY: docker.frappet.com
IMAGE_NAME: jws/jws-backend
DEPLOY_HOST: 49.0.91.80
COMPOSE_PATH: /home/frappet/docker/jws
jobs:
# act workflow_dispatch -W .github/workflows/release.yaml --input IMAGE_VER=test-v1 -s DOCKER_USER=sorawit -s DOCKER_PASS=P@ssword -s SSH_PASSWORD=P@ssw0rd
release-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# skip Set up QEMU because it fail on act and container
# Gen Version try to get version from tag or inut
- name: Set output tags
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Gen Version
id: gen_ver
run: |
if [[ $GITHUB_REF == 'refs/tags/'* ]]; then
IMAGE_VER=${{ steps.vars.outputs.tag }}
else
IMAGE_VER=${{ github.event.inputs.IMAGE_VER }}
fi
if [[ $IMAGE_VER == '' ]]; then
IMAGE_VER='test-vBeta'
fi
echo '::set-output name=image_ver::'$IMAGE_VER
- name: Check Version
run: |
echo $GITHUB_REF
echo ${{ steps.gen_ver.outputs.image_ver }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login in to registry
uses: docker/login-action@v2
with:
registry: ${{env.REGISTRY}}
username: ${{secrets.DOCKER_USER}}
password: ${{secrets.DOCKER_PASS}}
- name: Build and push docker image
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{ steps.gen_ver.outputs.image_ver }},${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest
- uses: snow-actions/line-notify@v1.1.0
if: success()
with:
access_token: ${{ secrets.TOKEN_LINE }}
message: |
-Success✅✅✅
Image: ${{env.IMAGE_NAME}}
Version: ${{ steps.gen_ver.outputs.IMAGE_VER }}
By: ${{ github.actor }}
- uses: snow-actions/line-notify@v1.1.0
if: failure()
with:
access_token: ${{ secrets.TOKEN_LINE }}
message: |
-Failure❌❌❌
Image: ${{env.IMAGE_NAME}}
Version: ${{ steps.gen_ver.outputs.IMAGE_VER }}
By: ${{ github.actor }}

View file

@ -4,6 +4,8 @@ ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable
RUN apt-get update && apt-get install -y openssl
RUN pnpm i -g prisma
WORKDIR /app WORKDIR /app
@ -11,6 +13,7 @@ COPY . .
FROM base AS deps FROM base AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
RUN pnpm prisma generate
FROM base AS build FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
@ -18,9 +21,13 @@ RUN pnpm prisma generate
RUN pnpm run build RUN pnpm run build
FROM base as prod FROM base as prod
ENV NODE_ENV="production" ENV NODE_ENV="production"
COPY --from=deps /app/node_modules /app/node_modules COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist COPY --from=build /app/dist /app/dist
COPY --from=base /app/static /app/static COPY --from=base /app/static /app/static
CMD ["pnpm", "run", "start"] RUN chmod u+x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

25
compose.yaml Normal file
View file

@ -0,0 +1,25 @@
version: "3.8"
services:
app:
build: .
restart: unless-stopped
ports:
- 3000:3000
environment:
- KC_URL=http://192.168.1.20:8080
- KC_REALM=dev
- KC_SERVICE_ACCOUNT_CLIENT_ID=
- KC_SERVICE_ACCOUNT_SECRET=
- APP_HOST=0.0.0.0
- APP_PORT=3000
- MINIO_HOST=192.168.1.20
- MINIO_PORT=9000
- MINIO_ACCESS_KEY=
- MINIO_SECRET_KEY=
- MINIO_BUCKET=jws-dev
- ELASTICSEARCH_PROTOCOL=http
- ELASTICSEARCH_HOST=192.168.1.20
- ELASTICSEARCH_PORT=9200
- ELASTICSEARCH_INDEX=jws-log-index
- DATABASE_URL=postgresql://postgres:1234@192.168.1.20:5432/dev_1?schema=public

4
entrypoint.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/sh
pnpm prisma migrate deploy
pnpm run start

View file

@ -1,5 +1,5 @@
{ {
"name": "template", "name": "jws-backend",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "./dist/app.js", "main": "./dist/app.js",
@ -24,12 +24,13 @@
"@types/swagger-ui-express": "^4.1.6", "@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.1.0", "nodemon": "^3.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prisma": "^5.11.0", "prisma": "^5.12.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.3" "typescript": "^5.4.3"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "5.11.0", "@elastic/elasticsearch": "^8.13.0",
"@prisma/client": "5.12.1",
"@tsoa/runtime": "^6.2.0", "@tsoa/runtime": "^6.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",

105
pnpm-lock.yaml generated
View file

@ -5,9 +5,12 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@elastic/elasticsearch':
specifier: ^8.13.0
version: 8.13.0
'@prisma/client': '@prisma/client':
specifier: 5.11.0 specifier: 5.12.1
version: 5.11.0(prisma@5.11.0) version: 5.12.1(prisma@5.12.1)
'@tsoa/runtime': '@tsoa/runtime':
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.0 version: 6.2.0
@ -56,8 +59,8 @@ devDependencies:
specifier: ^3.2.5 specifier: ^3.2.5
version: 3.2.5 version: 3.2.5
prisma: prisma:
specifier: ^5.11.0 specifier: ^5.12.1
version: 5.11.0 version: 5.12.1
ts-node: ts-node:
specifier: ^10.9.2 specifier: ^10.9.2
version: 10.9.2(@types/node@20.12.2)(typescript@5.4.3) version: 10.9.2(@types/node@20.12.2)(typescript@5.4.3)
@ -74,6 +77,30 @@ packages:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
dev: true dev: true
/@elastic/elasticsearch@8.13.0:
resolution: {integrity: sha512-OAYgzqArPqgDaIJ1yT0RX31YCgr1lleo53zL+36i23PFjHu08CA6Uq+BmBzEV05yEidl+ILPdeSfF3G8hPG/JQ==}
engines: {node: '>=18'}
dependencies:
'@elastic/transport': 8.5.0
tslib: 2.6.2
transitivePeerDependencies:
- supports-color
dev: false
/@elastic/transport@8.5.0:
resolution: {integrity: sha512-T+zSUHXBfrqlj/E9pJiaEgKoTdGykBCohzNBt6omDfI6EQtaNT240oMO03oXo35T8rwrCVonSMSoedbmToncVA==}
engines: {node: '>=18'}
dependencies:
debug: 4.3.4(supports-color@5.5.0)
hpagent: 1.2.0
ms: 2.1.3
secure-json-parse: 2.7.0
tslib: 2.6.2
undici: 6.11.1
transitivePeerDependencies:
- supports-color
dev: false
/@hapi/accept@6.0.3: /@hapi/accept@6.0.3:
resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==} resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==}
dependencies: dependencies:
@ -339,8 +366,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@prisma/client@5.11.0(prisma@5.11.0): /@prisma/client@5.12.1(prisma@5.12.1):
resolution: {integrity: sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==} resolution: {integrity: sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA==}
engines: {node: '>=16.13'} engines: {node: '>=16.13'}
requiresBuild: true requiresBuild: true
peerDependencies: peerDependencies:
@ -349,35 +376,35 @@ packages:
prisma: prisma:
optional: true optional: true
dependencies: dependencies:
prisma: 5.11.0 prisma: 5.12.1
dev: false dev: false
/@prisma/debug@5.11.0: /@prisma/debug@5.12.1:
resolution: {integrity: sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==} resolution: {integrity: sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A==}
/@prisma/engines-version@5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102: /@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab:
resolution: {integrity: sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==} resolution: {integrity: sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==}
/@prisma/engines@5.11.0: /@prisma/engines@5.12.1:
resolution: {integrity: sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==} resolution: {integrity: sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA==}
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/debug': 5.11.0 '@prisma/debug': 5.12.1
'@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102 '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab
'@prisma/fetch-engine': 5.11.0 '@prisma/fetch-engine': 5.12.1
'@prisma/get-platform': 5.11.0 '@prisma/get-platform': 5.12.1
/@prisma/fetch-engine@5.11.0: /@prisma/fetch-engine@5.12.1:
resolution: {integrity: sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==} resolution: {integrity: sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA==}
dependencies: dependencies:
'@prisma/debug': 5.11.0 '@prisma/debug': 5.12.1
'@prisma/engines-version': 5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102 '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab
'@prisma/get-platform': 5.11.0 '@prisma/get-platform': 5.12.1
/@prisma/get-platform@5.11.0: /@prisma/get-platform@5.12.1:
resolution: {integrity: sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==} resolution: {integrity: sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ==}
dependencies: dependencies:
'@prisma/debug': 5.11.0 '@prisma/debug': 5.12.1
/@tsconfig/node10@1.0.11: /@tsconfig/node10@1.0.11:
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@ -892,7 +919,6 @@ packages:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
supports-color: 5.5.0 supports-color: 5.5.0
dev: true
/decode-uri-component@0.2.2: /decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
@ -1322,7 +1348,6 @@ packages:
/has-flag@3.0.0: /has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true
/has-property-descriptors@1.0.2: /has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
@ -1354,6 +1379,11 @@ packages:
function-bind: 1.1.2 function-bind: 1.1.2
dev: false dev: false
/hpagent@1.2.0:
resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==}
engines: {node: '>=14'}
dev: false
/http-errors@2.0.0: /http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -1715,7 +1745,6 @@ packages:
/ms@2.1.2: /ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms@2.1.3: /ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1832,13 +1861,13 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/prisma@5.11.0: /prisma@5.12.1:
resolution: {integrity: sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==} resolution: {integrity: sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q==}
engines: {node: '>=16.13'} engines: {node: '>=16.13'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/engines': 5.11.0 '@prisma/engines': 5.12.1
/promise.any@2.0.6: /promise.any@2.0.6:
resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==} resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==}
@ -1963,6 +1992,10 @@ packages:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
dev: false dev: false
/secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
dev: false
/semver@7.6.0: /semver@7.6.0:
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2165,7 +2198,6 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
has-flag: 3.0.0 has-flag: 3.0.0
dev: true
/swagger-ui-dist@5.13.0: /swagger-ui-dist@5.13.0:
resolution: {integrity: sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q==} resolution: {integrity: sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q==}
@ -2242,6 +2274,10 @@ packages:
yn: 3.1.1 yn: 3.1.1
dev: true dev: true
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
/tsoa@6.2.0: /tsoa@6.2.0:
resolution: {integrity: sha512-EX/RyoU+4hD1rLM5NjYG+I7lEhqx1yuLgcHs/gyWQpkX/RL9cVR9hFA9LKQrK6PE+WTg1SEahn1MK3l/+6pVKw==} resolution: {integrity: sha512-EX/RyoU+4hD1rLM5NjYG+I7lEhqx1yuLgcHs/gyWQpkX/RL9cVR9hFA9LKQrK6PE+WTg1SEahn1MK3l/+6pVKw==}
engines: {node: '>=18.0.0', yarn: '>=1.9.4'} engines: {node: '>=18.0.0', yarn: '>=1.9.4'}
@ -2334,6 +2370,11 @@ packages:
/undici-types@5.26.5: /undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/undici@6.11.1:
resolution: {integrity: sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==}
engines: {node: '>=18.0'}
dev: false
/universalify@2.0.1: /universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}

View file

@ -0,0 +1,458 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('CREATED', 'ACTIVE', 'INACTIVE');
-- CreateEnum
CREATE TYPE "UserType" AS ENUM ('USER', 'MESSENGER', 'DELEGATE', 'AGENCY');
-- CreateTable
CREATE TABLE "Province" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Province_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "District" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"provinceId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "District_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SubDistrict" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"zipCode" TEXT NOT NULL,
"districtId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SubDistrict_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Branch" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"taxNo" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL,
"isHeadOffice" BOOLEAN NOT NULL DEFAULT false,
"headOfficeId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Branch_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchContact" (
"id" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"lineId" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchContact_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchUser" (
"id" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BranchUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"keycloakId" TEXT NOT NULL,
"code" TEXT,
"firstName" TEXT NOT NULL,
"firstNameEN" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"lastNameEN" TEXT NOT NULL,
"gender" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"registrationNo" TEXT,
"startDate" TIMESTAMP(3),
"retireDate" TIMESTAMP(3),
"userType" "UserType" NOT NULL,
"userRole" TEXT NOT NULL,
"discountCondition" TEXT,
"licenseNo" TEXT,
"licenseIssueDate" TIMESTAMP(3),
"licenseExpireDate" TIMESTAMP(3),
"sourceNationality" TEXT,
"importNationality" TEXT,
"trainingPlace" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Customer" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"customerType" TEXT NOT NULL,
"customerName" TEXT NOT NULL,
"customerNameEN" TEXT NOT NULL,
"imageUrl" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CustomerBranch" (
"id" TEXT NOT NULL,
"branchNo" TEXT NOT NULL,
"legalPersonNo" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameEN" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"taxNo" TEXT NOT NULL,
"registerName" TEXT NOT NULL,
"registerDate" TIMESTAMP(3) NOT NULL,
"authorizedCapital" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"latitude" TEXT NOT NULL,
"longitude" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerBranch_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Employee" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"nrcNo" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"firstNameEN" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"lastNameEN" TEXT NOT NULL,
"dateOfBirth" TIMESTAMP(3) NOT NULL,
"gender" TEXT NOT NULL,
"nationality" TEXT NOT NULL,
"address" TEXT NOT NULL,
"addressEN" TEXT NOT NULL,
"provinceId" TEXT,
"districtId" TEXT,
"subDistrictId" TEXT,
"zipCode" TEXT NOT NULL,
"email" TEXT NOT NULL,
"telephoneNo" TEXT NOT NULL,
"arrivalBarricade" TEXT NOT NULL,
"arrivalCardNo" TEXT NOT NULL,
"customerBranchId" TEXT,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Employee_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmployeeCheckup" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"checkupResult" TEXT NOT NULL,
"checkupType" TEXT NOT NULL,
"provinceId" TEXT,
"hospitalName" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"medicalBenefitScheme" TEXT NOT NULL,
"insuranceCompany" TEXT NOT NULL,
"coverageStartDate" TIMESTAMP(3) NOT NULL,
"coverageExpireDate" TIMESTAMP(3) NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeCheckup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmployeeWork" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"ownerName" TEXT NOT NULL,
"positionName" TEXT NOT NULL,
"jobType" TEXT NOT NULL,
"workplace" TEXT NOT NULL,
"workPermitNo" TEXT NOT NULL,
"workPermitIssuDate" TIMESTAMP(3) NOT NULL,
"workPermitExpireDate" TIMESTAMP(3) NOT NULL,
"workEndDate" TIMESTAMP(3) NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeWork_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "EmployeeOtherInfo" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"citizenId" TEXT NOT NULL,
"fatherFullName" TEXT NOT NULL,
"motherFullName" TEXT NOT NULL,
"birthPlace" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmployeeOtherInfo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Service" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Work" (
"id" TEXT NOT NULL,
"order" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Work_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WorkProduct" (
"id" TEXT NOT NULL,
"workId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WorkProduct_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductGroup" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductGroup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductType" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"remark" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"process" TEXT NOT NULL,
"price" INTEGER NOT NULL,
"agentPrice" INTEGER NOT NULL,
"serviceCharge" INTEGER NOT NULL,
"imageUrl" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'CREATED',
"productTypeId" TEXT,
"productGroupId" TEXT,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "District" ADD CONSTRAINT "District_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SubDistrict" ADD CONSTRAINT "SubDistrict_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_headOfficeId_fkey" FOREIGN KEY ("headOfficeId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchContact" ADD CONSTRAINT "BranchContact_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchUser" ADD CONSTRAINT "BranchUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerBranch" ADD CONSTRAINT "CustomerBranch_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_subDistrictId_fkey" FOREIGN KEY ("subDistrictId") REFERENCES "SubDistrict"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Employee" ADD CONSTRAINT "Employee_customerBranchId_fkey" FOREIGN KEY ("customerBranchId") REFERENCES "CustomerBranch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeCheckup" ADD CONSTRAINT "EmployeeCheckup_provinceId_fkey" FOREIGN KEY ("provinceId") REFERENCES "Province"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeWork" ADD CONSTRAINT "EmployeeWork_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EmployeeOtherInfo" ADD CONSTRAINT "EmployeeOtherInfo_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Work" ADD CONSTRAINT "Work_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WorkProduct" ADD CONSTRAINT "WorkProduct_workId_fkey" FOREIGN KEY ("workId") REFERENCES "Work"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_productTypeId_fkey" FOREIGN KEY ("productTypeId") REFERENCES "ProductType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the column `imageUrl` on the `Customer` table. All the data in the column will be lost.
- Changed the type of `customerType` on the `Customer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- CreateEnum
CREATE TYPE "CustomerType" AS ENUM ('CORP', 'PERS');
-- AlterTable
ALTER TABLE "Customer" DROP COLUMN "imageUrl",
DROP COLUMN "customerType",
ADD COLUMN "customerType" "CustomerType" NOT NULL;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "birtDate" TIMESTAMP(3),
ADD COLUMN "responsibleArea" TEXT;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `birtDate` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "birtDate",
ADD COLUMN "birthDate" TIMESTAMP(3);

View file

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `keycloakId` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "keycloakId";

View file

@ -0,0 +1,11 @@
/*
Warnings:
- You are about to drop the column `lineId` on the `BranchContact` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "lineId" TEXT;
-- AlterTable
ALTER TABLE "BranchContact" DROP COLUMN "lineId";

View file

@ -0,0 +1,119 @@
/*
Warnings:
- You are about to drop the column `telephoneNo` on the `Branch` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Branch" DROP COLUMN "telephoneNo";
-- CreateTable
CREATE TABLE "Menu" (
"id" TEXT NOT NULL,
"caption" TEXT NOT NULL,
"captionEN" TEXT NOT NULL,
"menuType" TEXT NOT NULL,
"url" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
"parentId" TEXT,
CONSTRAINT "Menu_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RoleMenuPermission" (
"id" TEXT NOT NULL,
"userRole" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RoleMenuPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserMenuPermission" (
"id" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserMenuPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MenuComponent" (
"id" TEXT NOT NULL,
"componentId" TEXT NOT NULL,
"componentTag" TEXT NOT NULL,
"menuId" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MenuComponent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RoleMenuComponentPermission" (
"id" TEXT NOT NULL,
"componentId" TEXT NOT NULL,
"componentTag" TEXT NOT NULL,
"menuComponentId" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RoleMenuComponentPermission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserMenuComponentPermission" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"menuComponentId" TEXT NOT NULL,
"permission" TEXT NOT NULL,
"createdBy" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updateBy" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserMenuComponentPermission_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Menu" ADD CONSTRAINT "Menu_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Menu"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleMenuPermission" ADD CONSTRAINT "RoleMenuPermission_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuPermission" ADD CONSTRAINT "UserMenuPermission_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuPermission" ADD CONSTRAINT "UserMenuPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MenuComponent" ADD CONSTRAINT "MenuComponent_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RoleMenuComponentPermission" ADD CONSTRAINT "RoleMenuComponentPermission_menuComponentId_fkey" FOREIGN KEY ("menuComponentId") REFERENCES "MenuComponent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuComponentPermission" ADD CONSTRAINT "UserMenuComponentPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserMenuComponentPermission" ADD CONSTRAINT "UserMenuComponentPermission_menuComponentId_fkey" FOREIGN KEY ("menuComponentId") REFERENCES "MenuComponent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "contactName" TEXT;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "checkpoint" TEXT,
ADD COLUMN "checkpointEN" TEXT;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "username" TEXT NOT NULL;

View file

@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `componentId` on the `RoleMenuComponentPermission` table. All the data in the column will be lost.
- You are about to drop the column `componentTag` on the `RoleMenuComponentPermission` table. All the data in the column will be lost.
- Added the required column `userRole` to the `RoleMenuComponentPermission` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "RoleMenuComponentPermission" DROP COLUMN "componentId",
DROP COLUMN "componentTag",
ADD COLUMN "userRole" TEXT NOT NULL;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "telephoneHq" TEXT;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `telephoneHq` on table `Branch` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Branch" ALTER COLUMN "telephoneHq" SET NOT NULL;

View file

@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `telephoneHq` on the `Branch` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Branch" DROP COLUMN "telephoneHq",
ADD COLUMN "telephoneNo" TEXT;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `telephoneNo` on table `Branch` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Branch" ALTER COLUMN "telephoneNo" SET NOT NULL;

View file

@ -7,9 +7,112 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model Menu {
id String @id @default(uuid())
caption String
captionEN String
menuType String
url String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
parent Menu? @relation(name: "MenuRelation", fields: [parentId], references: [id])
parentId String?
children Menu[] @relation(name: "MenuRelation")
roleMenuPermission RoleMenuPermission[]
userMenuPermission UserMenuPermission[]
userComponent MenuComponent[]
}
model RoleMenuPermission {
id String @id @default(uuid())
userRole String
permission String
menu Menu @relation(fields: [menuId], references: [id])
menuId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model UserMenuPermission {
id String @id @default(uuid())
permission String
menu Menu @relation(fields: [menuId], references: [id])
menuId String
user User @relation(fields: [userId], references: [id])
userId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model MenuComponent {
id String @id @default(uuid())
componentId String
componentTag String
menu Menu @relation(fields: [menuId], references: [id])
menuId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
roleMenuComponentPermission RoleMenuComponentPermission[]
userMennuComponentPermission UserMenuComponentPermission[]
}
model RoleMenuComponentPermission {
id String @id @default(uuid())
userRole String
permission String
menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id])
menuComponentId String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model UserMenuComponentPermission {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
menuComponent MenuComponent @relation(fields: [menuComponentId], references: [id])
menuComponentId String
permission String
createdBy String?
createdAt DateTime @default(now())
updateBy String?
updatedAt DateTime @updatedAt
}
model Province { model Province {
id String @id @default(uuid()) id String @id @default(uuid())
nameTH String name String
nameEN String nameEN String
createdBy String? createdBy String?
@ -27,7 +130,7 @@ model Province {
model District { model District {
id String @id @default(uuid()) id String @id @default(uuid())
nameTH String name String
nameEN String nameEN String
provinceId String provinceId String
@ -47,7 +150,7 @@ model District {
model SubDistrict { model SubDistrict {
id String @id @default(uuid()) id String @id @default(uuid())
nameTH String name String
nameEN String nameEN String
zipCode String zipCode String
@ -65,14 +168,21 @@ model SubDistrict {
employee Employee[] employee Employee[]
} }
enum Status {
CREATED
ACTIVE
INACTIVE
}
model Branch { model Branch {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String
taxNo String taxNo String
nameTH String name String
nameEN String nameEN String
addressTH String address String
addressEN String addressEN String
telephoneNo String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
provinceId String? provinceId String?
@ -86,7 +196,8 @@ model Branch {
zipCode String zipCode String
email String email String
telephoneNo String contactName String?
lineId String?
latitude String latitude String
longitude String longitude String
@ -96,7 +207,7 @@ model Branch {
headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id]) headOffice Branch? @relation(name: "HeadOfficeRelation", fields: [headOfficeId], references: [id])
headOfficeId String? headOfficeId String?
status String? status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -109,10 +220,8 @@ model Branch {
} }
model BranchContact { model BranchContact {
id String @id @default(uuid()) id String @id @default(uuid())
telephoneNo String telephoneNo String
lineId String
qrCodeImageUrl String?
branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade) branch Branch @relation(fields: [branchId], references: [id], onDelete: Cascade)
branchId String branchId String
@ -138,16 +247,25 @@ model BranchUser {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
enum UserType {
USER
MESSENGER
DELEGATE
AGENCY
}
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String?
firstNameTH String firstName String
firstNameEN String firstNameEN String
lastNameTH String lastName String
lastNameEN String lastNameEN String
username String
gender String
addressTH String address String
addressEN String addressEN String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
@ -164,46 +282,56 @@ model User {
email String email String
telephoneNo String telephoneNo String
registrationNo String registrationNo String?
startDate DateTime startDate DateTime?
retireDate DateTime retireDate DateTime?
profileImageUrl String? checkpoint String?
checkpointEN String?
userType String userType UserType
userRole String userRole String
discountCondition String discountCondition String?
licenseNo String licenseNo String?
licenseIssueDate DateTime licenseIssueDate DateTime?
licenseExpireDate DateTime licenseExpireDate DateTime?
sourceNationality String sourceNationality String?
importNationality String importNationality String?
trainingPlace String trainingPlace String?
responsibleArea String?
status String? birthDate DateTime?
status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updateBy String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
branch BranchUser[] branch BranchUser[]
userMenuPermission UserMenuPermission[]
userMenuComponentPermission UserMenuComponentPermission[]
}
enum CustomerType {
CORP
PERS
} }
model Customer { model Customer {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String
customerType String customerType CustomerType
customerNameTH String customerName String
customerNameEN String customerNameEN String
imageUrl String?
status String? status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -218,7 +346,7 @@ model CustomerBranch {
branchNo String branchNo String
legalPersonNo String legalPersonNo String
nameTH String name String
nameEN String nameEN String
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
@ -229,6 +357,7 @@ model CustomerBranch {
registerDate DateTime registerDate DateTime
authorizedCapital String authorizedCapital String
address String
addressEN String addressEN String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
@ -248,6 +377,8 @@ model CustomerBranch {
latitude String latitude String
longitude String longitude String
status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updateBy String?
@ -260,13 +391,17 @@ model Employee {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String
fullNameTH String nrcNo String
fullNameEN String firstName String
firstNameEN String
lastName String
lastNameEN String
dateOfBirth DateTime dateOfBirth DateTime
gender String gender String
nationality String nationality String
addressTH String address String
addressEN String addressEN String
province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull) province Province? @relation(fields: [provinceId], references: [id], onDelete: SetNull)
@ -285,12 +420,11 @@ model Employee {
arrivalBarricade String arrivalBarricade String
arrivalCardNo String arrivalCardNo String
profileImageUrl String
customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull) customerBranch CustomerBranch? @relation(fields: [customerBranchId], references: [id], onDelete: SetNull)
customerBranchId String? customerBranchId String?
status String? status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -299,7 +433,7 @@ model Employee {
employeeCheckup EmployeeCheckup[] employeeCheckup EmployeeCheckup[]
employeeWork EmployeeWork[] employeeWork EmployeeWork[]
EmployeeOtherInfo EmployeeOtherInfo[] employeeOtherInfo EmployeeOtherInfo[]
} }
model EmployeeCheckup { model EmployeeCheckup {
@ -368,11 +502,11 @@ model EmployeeOtherInfo {
model Service { model Service {
id String @id @default(uuid()) id String @id @default(uuid())
code String code String
name String name String
detail String detail String
imageUrl String
status String? status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -384,12 +518,14 @@ model Service {
model Work { model Work {
id String @id @default(uuid()) id String @id @default(uuid())
order String order Int
name String name String
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId String serviceId String
status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updateBy String? updateBy String?
@ -416,7 +552,8 @@ model ProductGroup {
name String name String
detail String detail String
remark String remark String
status String?
status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -432,7 +569,8 @@ model ProductType {
name String name String
detail String detail String
remark String remark String
status String?
status Status @default(CREATED)
createdBy String? createdBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -453,7 +591,8 @@ model Product {
agentPrice Int agentPrice Int
serviceCharge Int serviceCharge Int
imageUrl String imageUrl String
status String?
status Status @default(CREATED)
productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull)
productTypeId String? productTypeId String?

View file

@ -5,6 +5,7 @@ import swaggerUi from "swagger-ui-express";
import swaggerDocument from "./swagger.json"; import swaggerDocument from "./swagger.json";
import error from "./middlewares/error"; import error from "./middlewares/error";
import { RegisterRoutes } from "./routes"; import { RegisterRoutes } from "./routes";
import logMiddleware from "./middlewares/log";
const APP_HOST = process.env.APP_HOST || "0.0.0.0"; const APP_HOST = process.env.APP_HOST || "0.0.0.0";
const APP_PORT = +(process.env.APP_PORT || 3000); const APP_PORT = +(process.env.APP_PORT || 3000);
@ -15,11 +16,22 @@ const APP_PORT = +(process.env.APP_PORT || 3000);
app.use(cors()); app.use(cors());
app.use(json()); app.use(json());
app.use(urlencoded({ extended: true })); app.use(urlencoded({ extended: true }));
app.use(logMiddleware);
app.use("/", express.static("static")); app.use("/", express.static("static"));
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
RegisterRoutes(app); RegisterRoutes(app);
app.get("*", (_, res) =>
res.status(404).send({
status: 404,
message: "Route not found.",
devMessage: "unknown_url",
}),
);
app.use(error); app.use(error);
app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`)); app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));

View file

@ -0,0 +1,56 @@
import { Controller, Get, Path, Route, Tags } from "tsoa";
import prisma from "../db";
@Route("api/address")
@Tags("Address")
export class AddressController extends Controller {
@Get("province")
async getProvince() {
return await prisma.province.findMany();
}
@Get("province/{provinceId}")
async getProvinceById(@Path() provinceId: string) {
return await prisma.province.findFirst({
where: { id: provinceId },
});
}
@Get("province/{provinceId}/district")
async getDistrictOfProvince(@Path() provinceId: string) {
return await prisma.district.findMany({
where: { provinceId },
});
}
@Get("district")
async getDistrict() {
return await prisma.district.findMany();
}
@Get("district/{districtId}")
async getDistrictOfId(@Path() districtId: string) {
return await prisma.province.findFirst({
where: { id: districtId },
});
}
@Get("district/{districtId}/sub-district")
async getSubDistrictOfDistrict(@Path() districtId: string) {
return await prisma.subDistrict.findMany({
where: { districtId },
});
}
@Get("sub-district")
async getSubDistrict() {
return await prisma.subDistrict.findMany();
}
@Get("sub-district/{subDistrictId}")
async getSubDistrictOfId(@Path() subDistrictId: string) {
return await prisma.subDistrict.findFirst({
where: { id: subDistrictId },
});
}
}

View file

@ -0,0 +1,132 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type BranchContactCreate = {
telephoneNo: string;
};
type BranchContactUpdate = {
telephoneNo?: string;
};
@Route("api/branch/{branchId}/contact")
@Tags("Branch Contact")
@Security("keycloak")
export class BranchContactController extends Controller {
@Get()
async getBranchContact(
@Path() branchId: string,
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const [result, total] = await prisma.$transaction([
prisma.branchContact.findMany({
orderBy: { createdAt: "asc" },
where: { branchId },
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branchContact.count({ where: { branchId } }),
]);
return {
result,
page,
pageSize,
total,
};
}
@Get("{contactId}")
async getBranchContactById(@Path() branchId: string, @Path() contactId: string) {
const record = await prisma.branchContact.findFirst({ where: { id: contactId, branchId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Branch contact cannot be found.",
"data_not_found",
);
}
return record;
}
@Post()
async createBranchContact(
@Request() req: RequestWithUser,
@Path() branchId: string,
@Body() body: BranchContactCreate,
) {
if (!(await prisma.branch.findFirst({ where: { id: branchId } }))) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Branch not found.",
"missing_or_invalid_parameter",
);
}
const record = await prisma.branchContact.create({
data: { ...body, branchId, createdBy: req.user.name, updateBy: req.user.name },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{contactId}")
async editBranchContact(
@Request() req: RequestWithUser,
@Body() body: BranchContactUpdate,
@Path() branchId: string,
@Path() contactId: string,
) {
if (
!(await prisma.branchContact.findUnique({
where: { id: contactId, branchId },
}))
) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Branch contact cannot be found.",
"data_not_found",
);
}
const record = await prisma.branchContact.update({
data: { ...body, updateBy: req.user.name },
where: { id: contactId, branchId },
});
return record;
}
@Delete("{contactId}")
async deleteBranchContact(@Path() branchId: string, @Path() contactId: string) {
const result = await prisma.branchContact.deleteMany({ where: { id: contactId, branchId } });
if (result.count <= 0) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Branch contact cannot be found.",
"data_not_found",
);
}
}
}

View file

@ -0,0 +1,447 @@
import { Prisma, Status, UserType } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
import minio from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type BranchCreate = {
status?: Status;
taxNo: string;
nameEN: string;
name: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
contactName?: string | null;
contact?: string | string[] | null;
telephoneNo: string;
lineId?: string | null;
longitude: string;
latitude: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
headOfficeId?: string | null;
};
type BranchUpdate = {
status?: "ACTIVE" | "INACTIVE";
taxNo?: string;
nameEN?: string;
name?: string;
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo: string;
contactName?: string;
contact?: string | string[] | null;
lineId?: string;
longitude?: string;
latitude?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
headOfficeId?: string | null;
};
function lineImageLoc(id: string) {
return `branch/line-qr-${id}`;
}
function branchImageLoc(id: string) {
return `branch/branch-img-${id}`;
}
@Route("api/branch")
@Tags("Branch")
@Security("keycloak")
export class BranchController extends Controller {
@Get("stats")
async getStats() {
const list = await prisma.branch.groupBy({
_count: true,
by: "isHeadOffice",
});
return list.reduce<Record<"hq" | "br", number>>(
(a, c) => {
a[c.isHeadOffice ? "hq" : "br"] = c._count;
return a;
},
{ hq: 0, br: 0 },
);
}
@Get("user-stats")
async getUserStat(@Query() userType?: UserType) {
const list = await prisma.branchUser.groupBy({
_count: true,
where: { user: { userType } },
by: "branchId",
});
const record = await prisma.branch.findMany({
select: {
id: true,
nameEN: true,
name: true,
isHeadOffice: true,
},
});
return record.map((a) =>
Object.assign(a, {
count: list.find((b) => b.branchId === a.id)?._count ?? 0,
}),
);
}
@Get()
async getBranch(
@Query() zipCode?: string,
@Query() filter?: "head" | "sub",
@Query() headOfficeId?: string,
@Query() tree?: boolean,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
AND: {
headOfficeId: headOfficeId ?? (filter === "head" || tree ? null : undefined),
NOT: { headOfficeId: filter === "sub" && !headOfficeId ? null : undefined },
},
OR: [
{ nameEN: { contains: query }, zipCode },
{ name: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
{ telephoneNo: { contains: query }, zipCode },
],
} satisfies Prisma.BranchWhereInput;
const [result, total] = await prisma.$transaction([
prisma.branch.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
contact: true,
branch: tree && {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branch.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{branchId}")
async getBranchById(
@Path() branchId: string,
@Query() includeSubBranch?: boolean,
@Query() includeContact?: boolean,
) {
const record = await prisma.branch.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
branch: includeSubBranch && {
include: {
province: true,
district: true,
subDistrict: true,
},
},
contact: includeContact,
},
where: { id: branchId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
qrCodeImageUrl: await minio.presignedGetObject(MINIO_BUCKET, lineImageLoc(record.id)),
});
}
@Post()
async createBranch(@Request() req: RequestWithUser, @Body() body: BranchCreate) {
const [province, district, subDistrict, head] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.headOfficeId && !head)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Head branch cannot be found.",
"missing_or_invalid_parameter",
);
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
const year = new Date().getFullYear();
const last = await prisma.branch.findFirst({
orderBy: { createdAt: "desc" },
where: { headOfficeId: headOfficeId ?? null },
});
const code = !headOfficeId
? `HQ${year.toString().slice(2)}${+(last?.code.slice(-1) || 0) + 1}`
: `BR${head?.code.slice(2, 5)}${(+(last?.code.slice(-2) || 0) + 1).toString().padStart(2, "0")}`;
const record = await prisma.branch.create({
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
code,
isHeadOffice: !headOfficeId,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
headOffice: { connect: headOfficeId ? { id: headOfficeId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
if (headOfficeId) {
await prisma.branch.updateMany({
where: { id: headOfficeId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
if (record && contact) {
await prisma.branchContact.createMany({
data:
typeof contact === "string"
? [{ telephoneNo: contact, branchId: record.id }]
: contact.map((v) => ({ telephoneNo: v, branchId: record.id })),
});
}
return Object.assign(record, {
contact: await prisma.branchContact.findMany({ where: { branchId: record.id } }),
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
qrCodeImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
qrCodeImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
});
}
@Put("{branchId}")
async editBranch(
@Request() req: RequestWithUser,
@Body() body: BranchUpdate,
@Path() branchId: string,
) {
if (body.headOfficeId === branchId)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Cannot make this as head office and branch at the same time.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId || body.districtId || body.provinceId || body.headOfficeId) {
const [province, district, subDistrict, branch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.branch.findFirst({ where: { id: body.headOfficeId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.headOfficeId && !branch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Head branch cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, headOfficeId, contact, ...rest } = body;
if (!(await prisma.branch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const record = await prisma.branch.update({
include: { province: true, district: true, subDistrict: true },
data: {
...rest,
isHeadOffice: headOfficeId !== undefined ? headOfficeId === null : undefined,
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
headOffice: {
connect: headOfficeId ? { id: headOfficeId } : undefined,
disconnect: headOfficeId === null || undefined,
},
updateBy: req.user.name,
},
where: { id: branchId },
});
if (record && contact !== undefined) {
await prisma.branchContact.deleteMany({ where: { branchId } });
contact &&
(await prisma.branchContact.createMany({
data:
typeof contact === "string"
? [{ telephoneNo: contact, branchId }]
: contact.map((v) => ({ telephoneNo: v, branchId })),
}));
}
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, branchImageLoc(record.id)),
imageUploadUrl: await minio.presignedPutObject(MINIO_BUCKET, branchImageLoc(record.id)),
qrCodeImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
qrCodeImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
lineImageLoc(record.id),
12 * 60 * 60,
),
});
}
@Delete("{branchId}")
async deleteBranch(@Path() branchId: string) {
const record = await prisma.branch.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: branchId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Branch is in used.", "data_in_used");
}
await minio.removeObject(MINIO_BUCKET, lineImageLoc(branchId), {
forceDelete: true,
});
await minio.removeObject(MINIO_BUCKET, branchImageLoc(branchId), {
forceDelete: true,
});
return await prisma.branch.delete({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: branchId },
});
}
}

View file

@ -0,0 +1,290 @@
import { Prisma, Status, UserType } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type BranchUserBody = { user: string[] };
@Route("api/branch/{branchId}/user")
@Tags("Branch User")
@Security("keycloak")
export class BranchUserController extends Controller {
@Get()
async getBranchUser(
@Path() branchId: string,
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ user: { firstName: { contains: query }, zipCode }, branchId },
{ user: { firstNameEN: { contains: query }, zipCode }, branchId },
{ user: { lastName: { contains: query }, zipCode }, branchId },
{ user: { lastNameEN: { contains: query }, zipCode }, branchId },
{ user: { email: { contains: query }, zipCode }, branchId },
{ user: { telephoneNo: { contains: query }, zipCode }, branchId },
],
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.branchUser.findMany({
orderBy: { user: { createdAt: "asc" } },
include: {
user: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branchUser.count({ where }),
]);
return { result: result.map((v) => v.user), page, pageSize, total };
}
@Post()
async createBranchUser(
@Request() req: RequestWithUser,
@Path() branchId: string,
@Body() body: BranchUserBody,
) {
const [branch, user] = await prisma.$transaction([
prisma.branch.findUnique({
where: { id: branchId },
}),
prisma.user.findMany({
include: { branch: true },
where: { id: { in: body.user } },
}),
]);
if (!branch) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Branch cannot be found.",
"missing_or_invalid_parameter",
);
}
if (user.length !== body.user.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more user cannot be found.",
"missing_or_invalid_parameter",
);
}
await prisma.$transaction([
prisma.user.updateMany({
where: { id: { in: body.user }, status: Status.CREATED },
data: { status: Status.ACTIVE },
}),
prisma.branchUser.createMany({
data: user
.filter((a) => !a.branch.some((b) => b.branchId === branchId))
.map((v) => ({
branchId,
userId: v.id,
createdBy: req.user.name,
updateBy: req.user.name,
})),
}),
]);
const group: Record<UserType, string[]> = {
USER: [],
AGENCY: [],
DELEGATE: [],
MESSENGER: [],
};
for (const u of user) group[u.userType].push(u.id);
for (const g of Object.values(UserType)) {
if (group[g].length === 0) continue;
const last = await prisma.branchUser.findFirst({
orderBy: { createdAt: "desc" },
include: { user: true },
where: {
branchId,
user: {
userType: g,
code: { startsWith: `${branch.code.slice(4).padEnd(3, "0")}` },
},
},
});
const code = (idx: number) =>
`${branch.code.slice(4).padEnd(3, "0")}${g !== "USER" ? g.charAt(0) : ""}${(+(last?.user.code?.slice(-4) || 0) + idx + 1).toString().padStart(4, "0")}`;
await prisma.$transaction(
group[g].map((v, i) =>
prisma.user.updateMany({
where: { id: v, code: null },
data: { code: code(i) },
}),
),
);
}
}
@Delete()
async deleteBranchUser(@Path() branchId: string, @Body() body: BranchUserBody) {
await prisma.$transaction(
body.user.map((v) => prisma.branchUser.deleteMany({ where: { branchId, userId: v } })),
);
await prisma.user.updateMany({
where: {
branch: { none: {} },
},
data: { code: null },
});
}
@Delete("{userId}")
async deleteBranchUserById(@Path() branchId: string, @Path() userId: string) {
await prisma.branchUser.deleteMany({
where: { branchId, userId },
});
await prisma.user.updateMany({
where: {
id: userId,
branch: { none: {} },
},
data: { code: null },
});
}
}
type UserBranchBody = { branch: string[] };
@Route("api/user/{userId}/branch")
@Tags("Branch User")
@Security("keycloak")
export class UserBranchController extends Controller {
@Get()
async getUserBranch(
@Path() userId: string,
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ branch: { name: { contains: query }, zipCode }, userId },
{ branch: { nameEN: { contains: query }, zipCode }, userId },
],
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.branchUser.findMany({
orderBy: { branch: { createdAt: "asc" } },
include: {
branch: {
include: {
province: true,
district: true,
subDistrict: true,
},
},
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.branchUser.count({ where }),
]);
return { result: result.map((v) => v.branch), page, pageSize, total };
}
@Post()
async createUserBranch(
@Request() req: RequestWithUser,
@Path() userId: string,
@Body() body: UserBranchBody,
) {
const branch = await prisma.branch.findMany({
include: { user: true },
where: { id: { in: body.branch } },
});
if (branch.length !== body.branch.length) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"One or more branch cannot be found.",
"missing_or_invalid_parameter",
);
}
await prisma.branch.updateMany({
where: { id: { in: body.branch }, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
await prisma.branchUser.createMany({
data: branch
.filter((a) => !a.user.some((b) => b.userId === userId))
.map((v) => ({
branchId: v.id,
userId,
createdBy: req.user.name,
updateBy: req.user.name,
})),
});
this.setStatus(HttpStatus.CREATED);
}
@Delete()
async deleteUserBranch(@Path() userId: string, @Body() body: UserBranchBody) {
await prisma.$transaction(
body.branch.map((v) => prisma.branchUser.deleteMany({ where: { userId, branchId: v } })),
);
await prisma.user.updateMany({
where: {
branch: { none: {} },
},
data: { code: null },
});
}
@Delete("{branchId}")
async deleteUserBranchById(@Path() branchId: string, @Path() userId: string) {
await prisma.branchUser.deleteMany({
where: { branchId, userId },
});
await prisma.user.updateMany({
where: {
id: userId,
branch: { none: {} },
},
data: { code: null },
});
}
}

View file

@ -0,0 +1,356 @@
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import minio from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
function imageLocation(id: string) {
return `employee/profile-img-${id}`;
}
type CustomerBranchCreate = {
customerId: string;
status?: Status;
legalPersonNo: string;
taxNo: string;
name: string;
nameEN: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
longitude: string;
latitude: string;
registerName: string;
registerDate: Date;
authorizedCapital: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type CustomerBranchUpdate = {
customerId?: string;
status?: "ACTIVE" | "INACTIVE";
legalPersonNo?: string;
taxNo?: string;
name?: string;
nameEN?: string;
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
longitude?: string;
latitude?: string;
registerName?: string;
registerDate?: Date;
authorizedCapital?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
@Route("api/customer-branch")
@Tags("Customer Branch")
@Security("keycloak")
export class CustomerBranchController extends Controller {
@Get()
async list(
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ nameEN: { contains: query }, zipCode },
{ name: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.BranchWhereInput;
const [result, total] = await prisma.$transaction([
prisma.customerBranch.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.customerBranch.count({ where }),
]);
return { result, page, pageSize, total };
}
@Get("{branchId}")
async getById(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: branchId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
return record;
}
@Get("{branchId}/employee")
async listEmployee(
@Path() branchId: string,
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
AND: { customerBranchId: branchId },
OR: [
{ firstName: { contains: query }, zipCode },
{ firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.employee.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.employee.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) {
const [province, district, subDistrict, customer] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerId && !customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
const count = await prisma.customerBranch.count({
where: { customerId },
});
const record = await prisma.customerBranch.create({
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
branchNo: `${count + 1}`,
customer: { connect: { id: customerId } },
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
await prisma.customer.updateMany({
where: { id: customerId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{branchId}")
async editById(
@Request() req: RequestWithUser,
@Body() body: CustomerBranchUpdate,
@Path() branchId: string,
) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) {
const [province, district, subDistrict, customer] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customer.findFirst({ where: { id: body.customerId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerId && !customer)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerId, ...rest } = body;
if (!(await prisma.customerBranch.findUnique({ where: { id: branchId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const record = await prisma.customerBranch.update({
where: { id: branchId },
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
customer: { connect: customerId ? { id: customerId } : undefined },
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{branchId}")
async delete(@Path() branchId: string) {
const record = await prisma.customerBranch.findFirst({ where: { id: branchId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Customer branch cannot be found.",
"data_not_found",
);
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Customer branch is in used.", "data_in_used");
}
return await prisma.customerBranch.delete({ where: { id: branchId } });
}
}

View file

@ -0,0 +1,178 @@
import { CustomerType, Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import minio from "../services/minio";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
export type CustomerCreate = {
status?: Status;
customerType: CustomerType;
customerName: string;
customerNameEN: string;
};
export type CustomerUpdate = {
status?: "ACTIVE" | "INACTIVE";
customerType?: CustomerType;
customerName?: string;
customerNameEN?: string;
};
function imageLocation(id: string) {
return `customer/img-${id}`;
}
@Route("api/customer")
@Tags("Customer")
@Security("keycloak")
export class CustomerController extends Controller {
@Get()
async list(
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [{ customerName: { contains: query } }, { customerNameEN: { contains: query } }],
} satisfies Prisma.CustomerWhereInput;
const [result, total] = await prisma.$transaction([
prisma.customer.findMany({
orderBy: { createdAt: "asc" },
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.customer.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
imageUrl: await minio.presignedGetObject(MINIO_BUCKET, imageLocation(v.id), 12 * 60 * 60),
})),
),
page,
pageSize,
total,
};
}
@Get("{customerId}")
async getById(@Path() customerId: string) {
const record = await prisma.customer.findFirst({ where: { id: customerId } });
if (!record)
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Post()
async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) {
const last = await prisma.customer.findFirst({
orderBy: { createdAt: "desc" },
where: { customerType: body.customerType },
});
const code = `${body.customerType}${(+(last?.code.slice(-6) || 0) + 1).toString().padStart(6, "0")}`;
const record = await prisma.customer.create({
data: {
...body,
code,
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{customerId}")
async editById(
@Path() customerId: string,
@Request() req: RequestWithUser,
@Body() body: CustomerUpdate,
) {
if (!(await prisma.customer.findUnique({ where: { id: customerId } }))) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
}
const record = await prisma.customer.update({
where: { id: customerId },
data: {
...body,
createdBy: req.user.name,
updateBy: req.user.name,
},
});
return Object.assign(record, {
imageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
imageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{customerId}")
async deleteById(@Path() customerId: string) {
const record = await prisma.customer.findFirst({ where: { id: customerId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "Customer is in used.", "data_in_used");
}
return await prisma.customer.delete({ where: { id: customerId } });
}
}

View file

@ -0,0 +1,183 @@
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
type EmployeeCheckupCreate = {
checkupType: string;
checkupResult: string;
provinceId?: string | null;
hospitalName: string;
remark: string;
medicalBenefitScheme: string;
insuranceCompany: string;
coverageStartDate: Date;
coverageExpireDate: Date;
};
type EmployeeCheckupEdit = {
checkupType?: string;
checkupResult?: string;
provinceId?: string | null;
hospitalName?: string;
remark?: string;
medicalBenefitScheme?: string;
insuranceCompany?: string;
coverageStartDate?: Date;
coverageExpireDate?: Date;
};
@Route("api/employee/{employeeId}/checkup")
@Tags("Employee Checkup")
@Security("keycloak")
export class EmployeeCheckupController extends Controller {
@Get()
async list(@Path() employeeId: string) {
return prisma.employeeCheckup.findMany({
orderBy: { createdAt: "asc" },
where: { employeeId },
});
}
@Get("{checkupId}")
async getById(@Path() employeeId: string, @Path() checkupId: string) {
const record = await prisma.employeeCheckup.findFirst({
where: { id: checkupId, employeeId },
});
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
);
}
return record;
}
@Post()
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeCheckupCreate,
) {
if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.employee.findFirst({ where: { id: employeeId } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (!employee)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, ...rest } = body;
const record = await prisma.employeeCheckup.create({
include: { province: true },
data: {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{checkupId}")
async editById(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() checkupId: string,
@Body() body: EmployeeCheckupEdit,
) {
if (body.provinceId || employeeId) {
const [province, employee] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.employee.findFirst({ where: { id: employeeId } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (!employee)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, ...rest } = body;
if (!(await prisma.employeeCheckup.findUnique({ where: { id: checkupId, employeeId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
);
}
const record = await prisma.employeeCheckup.update({
include: { province: true },
where: { id: checkupId, employeeId },
data: {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{checkupId}")
async deleteById(@Path() employeeId: string, @Path() checkupId: string) {
const record = await prisma.employeeCheckup.findFirst({ where: { id: checkupId, employeeId } });
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee checkup cannot be found.",
"data_not_found",
);
}
return await prisma.employeeCheckup.delete({ where: { id: checkupId, employeeId } });
}
}

View file

@ -0,0 +1,330 @@
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Path,
Post,
Put,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { RequestWithUser } from "../interfaces/user";
import prisma from "../db";
import HttpStatus from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
import minio from "../services/minio";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
function imageLocation(id: string) {
return `employee/profile-img-${id}`;
}
type EmployeeCreate = {
customerBranchId: string;
status?: Status;
code: string;
nrcNo: string;
dateOfBirth: Date;
gender: string;
nationality: string;
firstName: string;
firstNameEN: string;
lastName: string;
lastNameEN: string;
addressEN: string;
address: string;
zipCode: string;
email: string;
telephoneNo: string;
arrivalBarricade: string;
arrivalCardNo: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type EmployeeUpdate = {
customerBranchId?: string;
status?: "ACTIVE" | "INACTIVE";
code?: string;
nrcNo?: string;
dateOfBirth?: Date;
gender?: string;
nationality?: string;
firstName?: string;
firstNameEN?: string;
lastName?: string;
lastNameEN?: string;
addressEN?: string;
address?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
arrivalBarricade?: string;
arrivalCardNo?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
@Route("api/employee")
@Tags("Employee")
@Security("keycloak")
export class EmployeeController extends Controller {
@Get()
async list(
@Query() zipCode?: string,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ firstName: { contains: query }, zipCode },
{ firstNameEN: { contains: query }, zipCode },
{ lastName: { contains: query }, zipCode },
{ lastNameEN: { contains: query }, zipCode },
{ email: { contains: query }, zipCode },
],
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
prisma.employee.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.employee.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{employeeId}")
async getById(@Path() employeeId: string) {
const record = await prisma.employee.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: employeeId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found");
}
return record;
}
@Post()
async create(@Request() req: RequestWithUser, @Body() body: EmployeeCreate) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) {
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerBranchId && !customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer Branch cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body;
const record = await prisma.employee.create({
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
customerBranch: { connect: { id: customerBranchId } },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
await prisma.customerBranch.updateMany({
where: { id: customerBranchId, status: Status.CREATED },
data: { status: Status.ACTIVE },
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{employeeId}")
async editById(
@Request() req: RequestWithUser,
@Body() body: EmployeeUpdate,
@Path() employeeId: string,
) {
if (body.provinceId || body.districtId || body.subDistrictId || body.customerBranchId) {
const [province, district, subDistrict, customerBranch] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
prisma.customerBranch.findFirst({ where: { id: body.customerBranchId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
if (body.customerBranchId && !customerBranch)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Customer cannot be found.",
"missing_or_invalid_parameter",
);
}
const { provinceId, districtId, subDistrictId, customerBranchId, ...rest } = body;
const record = await prisma.employee.update({
where: { id: employeeId },
include: {
province: true,
district: true,
subDistrict: true,
},
data: {
...rest,
customerBranch: { connect: customerBranchId ? { id: customerBranchId } : undefined },
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{employeeId}")
async delete(@Path() employeeId: string) {
const record = await prisma.employee.findFirst({ where: { id: employeeId } });
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(
HttpStatus.FORBIDDEN,
"Emplyee is in used.",
"missing_or_invalid_parameter",
);
}
return await prisma.employee.delete({ where: { id: employeeId } });
}
}

View file

@ -0,0 +1,127 @@
import { Prisma, Status } from "@prisma/client";
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
type EmployeeOtherInfoCreate = {
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
};
type EmployeeOtherInfoUpdate = {
citizenId: string;
fatherFullName: string;
motherFullName: string;
birthPlace: string;
};
@Route("api/employee/{employeeId}/other-info")
@Tags("Employee Other Info")
@Security("keycloak")
export class EmployeeOtherInfo extends Controller {
@Get()
async list(@Path() employeeId: string) {
return prisma.employeeOtherInfo.findMany({
orderBy: { createdAt: "asc" },
where: { employeeId },
});
}
@Get("{otherInfoId}")
async getById(@Path() employeeId: string, @Path() otherInfoId: string) {
const record = await prisma.employeeOtherInfo.findFirst({
where: { id: otherInfoId, employeeId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "Employee info cannot be found.", "data_not_found");
}
return record;
}
@Post()
async create(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Body() body: EmployeeOtherInfoCreate,
) {
if (!(await prisma.employee.findUnique({ where: { id: employeeId } })))
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Employee cannot be found.",
"missing_or_invalid_parameter",
);
const record = await prisma.employeeOtherInfo.create({
data: {
...body,
employee: { connect: { id: employeeId } },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{otherInfoId}")
async editById(
@Request() req: RequestWithUser,
@Path() employeeId: string,
@Path() otherInfoId: string,
@Body() body: EmployeeOtherInfoUpdate,
) {
if (!(await prisma.employeeOtherInfo.findUnique({ where: { id: otherInfoId, employeeId } }))) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee other info cannot be found.",
"data_not_found",
);
}
const record = await prisma.employeeOtherInfo.update({
where: { id: otherInfoId, employeeId },
data: { ...body, createdBy: req.user.name, updateBy: req.user.name },
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Delete("{otherInfoId}")
async deleteById(@Path() employeeId: string, @Path() otherInfoId: string) {
const record = await prisma.employeeOtherInfo.findFirst({
where: { id: otherInfoId, employeeId },
});
if (!record) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Employee other info cannot be found.",
"data_not_found",
);
}
return await prisma.employeeOtherInfo.delete({ where: { id: otherInfoId, employeeId } });
}
}

View file

@ -0,0 +1,76 @@
import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } from "tsoa";
import {
addUserRoles,
createUser,
deleteUser,
editUser,
getRoles,
removeUserRoles,
} from "../services/keycloak";
@Route("api/keycloak")
@Tags("Single-Sign On")
@Security("keycloak")
export class KeycloakController extends Controller {
@Post("user")
async createUser(
@Body() body: { username: string; password: string; firstName?: string; lastName?: string },
) {
return await createUser(body.username, body.password, {
firstName: body.firstName,
lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"],
});
}
@Put("user/{userId}")
async editUser(
@Path() userId: string,
@Body() body: { username?: string; password?: string; firstName?: string; lastName?: string },
) {
return await editUser(userId, body);
}
@Delete("user/{userId}")
async deleteUser(@Path() userId: string) {
return await deleteUser(userId);
}
@Get("role")
async getRole() {
const role = await getRoles();
if (Array.isArray(role))
return role.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)),
);
throw new Error("Failed. Cannot get role.");
}
@Post("{userId}/role")
async addRole(@Path() userId: string, @Body() body: { role: string[] }) {
const list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
const result = await addUserRoles(
userId,
list.filter((v) => body.role.includes(v.id)),
);
if (!result) throw new Error("Failed. Cannot set user's role.");
}
@Delete("{userId}/role/{roleId}")
async deleteRole(@Path() userId: string, @Path() roleId: string) {
const list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
const result = await removeUserRoles(
userId,
list.filter((v) => roleId === v.id),
);
if (!result) throw new Error("Failed. Cannot remove user's role.");
}
}

View file

@ -0,0 +1,178 @@
import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security, Tags } from "tsoa";
import prisma from "../db";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
type MenuCreate = {
caption: string;
captionEN: string;
menuType: string;
url: string;
parentId?: string;
};
type MenuEdit = {
caption: string;
captionEN: string;
menuType: string;
url: string;
};
@Route("v1/permission/menu")
@Tags("Permission")
@Security("keycloak")
export class MenuController extends Controller {
@Get()
async listMenu() {
const record = await prisma.menu.findMany({
include: { children: true, roleMenuPermission: true },
orderBy: { createdAt: "asc" },
});
return record;
}
@Post()
async createMenu(@Body() body: MenuCreate) {
if (body.parentId) {
const parent = await prisma.menu.findFirst({ where: { id: body.parentId } });
if (!parent) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Parent menu not found.",
"missing_or_invalid_parameter",
);
}
}
const record = await prisma.menu.create({
include: { parent: true },
data: body,
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{menuId}")
async editMenu(@Path("menuId") id: string, @Body() body: MenuEdit) {
const record = await prisma.menu
.update({
include: { parent: true },
where: { id },
data: body,
})
.catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found");
}
throw new Error(e);
});
return record;
}
@Delete("{menuId}")
async deleteMenu(@Path("menuId") id: string) {
const record = await prisma.menu.deleteMany({ where: { id } });
if (record.count <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "Menu cannot be found.", "data_not_found");
}
}
}
type MenuComponentCreate = {
componentId: string;
componentTag: string;
menuId: string;
};
type MenuComponentEdit = {
componentId?: string;
componentTag?: string;
menuId?: string;
};
@Route("v1/permission/menu-component")
@Tags("Permission")
@Security("keycloak")
export class MenuComponentController extends Controller {
@Get()
async listMenuComponent() {
const record = await prisma.menuComponent.findMany({
include: { roleMenuComponentPermission: true },
orderBy: { createdAt: "asc" },
});
return record;
}
@Post()
async createMenuComponent(@Body() body: MenuComponentCreate) {
const menu = await prisma.menu.findFirst({ where: { id: body.menuId } });
if (!menu) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"missing_or_invalid_parameter",
);
}
const record = await prisma.menuComponent.create({
data: body,
});
this.setStatus(HttpStatus.CREATED);
return record;
}
@Put("{menuComponentId}")
async editMenuComponent(@Path("menuComponentId") id: string, @Body() body: MenuComponentEdit) {
if (body.menuId) {
const menu = await prisma.menu.findFirst({ where: { id: body.menuId } });
if (!menu) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Menu not found.",
"missing_or_invalid_parameter",
);
}
}
const record = await prisma.menuComponent
.update({
include: { roleMenuComponentPermission: true },
where: { id },
data: body,
})
.catch((e) => {
if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Menu component cannot be found.",
"data_not_found",
);
}
throw new Error(e);
});
return record;
}
@Delete("{menuComponentId}")
async deleteMenuComponent(@Path("menuComponentId") id: string) {
const record = await prisma.menuComponent.deleteMany({ where: { id } });
if (record.count <= 0) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Menu component cannot be found.",
"data_not_found",
);
}
}
}

View file

@ -0,0 +1,576 @@
import {
Body,
Controller,
Delete,
Get,
Put,
Path,
Post,
Query,
Request,
Route,
Security,
Tags,
} from "tsoa";
import { Prisma, Status, UserType } from "@prisma/client";
import prisma from "../db";
import minio from "../services/minio";
import { RequestWithUser } from "../interfaces/user";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import {
addUserRoles,
createUser,
deleteUser,
editUser,
getRoles,
getUserRoles,
removeUserRoles,
} from "../services/keycloak";
if (!process.env.MINIO_BUCKET) {
throw Error("Require MinIO bucket.");
}
const MINIO_BUCKET = process.env.MINIO_BUCKET;
type UserCreate = {
status?: Status;
userType: UserType;
userRole: string;
username: string;
firstName: string;
firstNameEN: string;
lastName: string;
lastNameEN: string;
gender: string;
checkpoint?: string | null;
checkpointEN?: string | null;
registrationNo?: string | null;
startDate?: Date | null;
retireDate?: Date | null;
discountCondition?: string | null;
licenseNo?: string | null;
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string | null;
birthDate?: Date | null;
address: string;
addressEN: string;
zipCode: string;
email: string;
telephoneNo: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
type UserUpdate = {
status?: "ACTIVE" | "INACTIVE";
username?: string;
userType?: UserType;
userRole?: string;
firstName?: string;
firstNameEN?: string;
lastName?: string;
lastNameEN?: string;
gender?: string;
checkpoint?: string | null;
checkpointEN?: string | null;
registrationNo?: string | null;
startDate?: Date | null;
retireDate?: Date | null;
discountCondition?: string | null;
licenseNo?: string | null;
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
trainingPlace?: string | null;
responsibleArea?: string | null;
birthDate?: Date | null;
address?: string;
addressEN?: string;
zipCode?: string;
email?: string;
telephoneNo?: string;
subDistrictId?: string | null;
districtId?: string | null;
provinceId?: string | null;
};
function imageLocation(id: string) {
return `user/profile-img-${id}`;
}
@Route("api/user")
@Tags("User")
@Security("keycloak")
export class UserController extends Controller {
@Get("type-stats")
async getUserTypeStats() {
const list = await prisma.user.groupBy({
by: "userType",
_count: true,
});
return list.reduce<Record<UserType, number>>(
(a, c) => {
a[c.userType] = c._count;
return a;
},
{
USER: 0,
MESSENGER: 0,
DELEGATE: 0,
AGENCY: 0,
},
);
}
@Get()
async getUser(
@Query() userType?: UserType,
@Query() zipCode?: string,
@Query() includeBranch: boolean = false,
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
) {
const where = {
OR: [
{ firstName: { contains: query }, zipCode, userType },
{ firstNameEN: { contains: query }, zipCode, userType },
{ lastName: { contains: query }, zipCode, userType },
{ lastNameEN: { contains: query }, zipCode, userType },
{ email: { contains: query }, zipCode, userType },
{ telephoneNo: { contains: query }, zipCode, userType },
],
} satisfies Prisma.UserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.user.findMany({
orderBy: { createdAt: "asc" },
include: {
province: true,
district: true,
subDistrict: true,
branch: { include: { branch: includeBranch } },
},
where,
take: pageSize,
skip: (page - 1) * pageSize,
}),
prisma.user.count({ where }),
]);
return {
result: await Promise.all(
result.map(async (v) => ({
...v,
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(v.id),
12 * 60 * 60,
),
})),
),
page,
pageSize,
total,
};
}
@Get("{userId}")
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record)
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
60 * 60,
),
});
}
@Post()
async createUser(@Request() req: RequestWithUser, @Body() body: UserCreate) {
if (body.provinceId || body.districtId || body.subDistrictId) {
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId ?? undefined } }),
prisma.district.findFirst({ where: { id: body.districtId ?? undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId ?? undefined } }),
]);
if (body.provinceId && !province) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
}
if (body.districtId && !district) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
}
if (body.subDistrictId && !subDistrict) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
}
}
const { provinceId, districtId, subDistrictId, username, ...rest } = body;
let list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
if (Array.isArray(list)) {
list = list.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) => a.name.includes(b)),
);
}
const userId = await createUser(username, username, {
firstName: body.firstName,
lastName: body.lastName,
requiredActions: ["UPDATE_PASSWORD"],
});
if (!userId || typeof userId !== "string") {
throw new Error("Cannot create user with keycloak service.");
}
const role = list.find((v) => v.name === body.userRole);
const resultAddRole = role && (await addUserRoles(userId, [role]));
if (!resultAddRole) {
await deleteUser(userId);
throw new Error("Failed. Cannot set user's role.");
}
const record = await prisma.user.create({
include: { province: true, district: true, subDistrict: true },
data: {
id: userId,
...rest,
username,
userRole: role.name,
province: { connect: provinceId ? { id: provinceId } : undefined },
district: { connect: districtId ? { id: districtId } : undefined },
subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined },
createdBy: req.user.name,
updateBy: req.user.name,
},
});
this.setStatus(HttpStatus.CREATED);
return Object.assign(record, {
profileImageUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Put("{userId}")
async editUser(
@Request() req: RequestWithUser,
@Body() body: UserUpdate,
@Path() userId: string,
) {
if (body.subDistrictId || body.districtId || body.provinceId) {
const [province, district, subDistrict] = await prisma.$transaction([
prisma.province.findFirst({ where: { id: body.provinceId || undefined } }),
prisma.district.findFirst({ where: { id: body.districtId || undefined } }),
prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }),
]);
if (body.provinceId && !province)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Province cannot be found.",
"missing_or_invalid_parameter",
);
if (body.districtId && !district)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"District cannot be found.",
"missing_or_invalid_parameter",
);
if (body.subDistrictId && !subDistrict)
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Sub-district cannot be found.",
"missing_or_invalid_parameter",
);
}
let userRole: string | undefined;
if (body.userRole) {
let list = await getRoles();
if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
if (Array.isArray(list)) {
list = list.filter(
(a) =>
!["uma_authorization", "offline_access", "default-roles"].some((b) =>
a.name.includes(b),
),
);
}
const currentRole = await getUserRoles(userId);
const role = list.find((v) => v.name === body.userRole);
const resultAddRole = role && (await addUserRoles(userId, [role]));
if (!resultAddRole) {
throw new Error("Failed. Cannot set user's role.");
} else {
if (Array.isArray(currentRole)) await removeUserRoles(userId, currentRole);
}
userRole = role.name;
}
if (body.username) {
await editUser(userId, { username: body.username });
}
const { provinceId, districtId, subDistrictId, ...rest } = body;
const user = await prisma.user.findFirst({
where: { id: userId },
});
if (!user) {
throw new HttpError(HttpStatus.NOT_FOUND, "Branch cannot be found.", "data_not_found");
}
const lastUserOfType =
body.userType &&
body.userType !== user.userType &&
user.code &&
(await prisma.user.findFirst({
orderBy: { createdAt: "desc" },
where: {
userType: body.userType,
code: { startsWith: `${user.code?.slice(0, 3)}` },
},
}));
const record = await prisma.user.update({
include: { province: true, district: true, subDistrict: true },
data: {
...rest,
userRole,
code:
(lastUserOfType &&
`${user.code?.slice(0, 3)}${body.userType !== "USER" ? body.userType?.charAt(0) : ""}${(+(lastUserOfType?.code?.slice(-4) || 0) + 1).toString().padStart(4, "0")}`) ||
undefined,
province: {
connect: provinceId ? { id: provinceId } : undefined,
disconnect: provinceId === null || undefined,
},
district: {
connect: districtId ? { id: districtId } : undefined,
disconnect: districtId === null || undefined,
},
subDistrict: {
connect: subDistrictId ? { id: subDistrictId } : undefined,
disconnect: subDistrictId === null || undefined,
},
updateBy: req.user.name,
},
where: { id: userId },
});
return Object.assign(record, {
profileImageUrl: await minio.presignedGetObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
profileImageUploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
imageLocation(record.id),
12 * 60 * 60,
),
});
}
@Delete("{userId}")
async deleteUser(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
if (record.status !== Status.CREATED) {
throw new HttpError(HttpStatus.FORBIDDEN, "User is in used.", "data_in_used");
}
await minio.removeObject(MINIO_BUCKET, imageLocation(userId), {
forceDelete: true,
});
new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`);
stream.on("data", (v) => v && v.name && item.push(v.name));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("MinIO error.")));
}).then((list) => {
list.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
});
});
await deleteUser(userId);
return await prisma.user.delete({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
}
}
function attachmentLocation(uid: string) {
return `user-attachment/${uid}`;
}
@Route("api/user/{userId}/attachment")
@Tags("User")
@Security("keycloak")
export class UserAttachmentController extends Controller {
@Get()
async listAttachment(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
const list = await new Promise<string[]>((resolve, reject) => {
const item: string[] = [];
const stream = minio.listObjectsV2(MINIO_BUCKET, `${attachmentLocation(userId)}/`);
stream.on("data", (v) => v && v.name && item.push(v.name));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("MinIO error.")));
});
return await Promise.all(
list.map(async (v) => ({
name: v.split("/").at(-1) as string,
url: await minio.presignedGetObject(MINIO_BUCKET, v, 12 * 60 * 60),
})),
);
}
@Post()
async addAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
const record = await prisma.user.findFirst({
include: {
province: true,
district: true,
subDistrict: true,
},
where: { id: userId },
});
if (!record) {
throw new HttpError(HttpStatus.NOT_FOUND, "User cannot be found.", "data_not_found");
}
return await Promise.all(
payload.file.map(async (v) => ({
name: v,
url: await minio.presignedGetObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`),
uploadUrl: await minio.presignedPutObject(
MINIO_BUCKET,
`${attachmentLocation(userId)}/${v}`,
12 * 60 * 60,
),
})),
);
}
@Delete()
async deleteAttachment(@Path() userId: string, @Body() payload: { file: string[] }) {
await Promise.all(
payload.file.map(async (v) => {
await minio.removeObject(MINIO_BUCKET, `${attachmentLocation(userId)}/${v}`, {
forceDelete: true,
});
}),
);
}
}

View file

@ -1,18 +1,29 @@
import HttpStatus from "./http-status"; import HttpStatus from "./http-status";
type DevMessage =
| "missing_or_invalid_parameter"
| "data_exists"
| "data_in_used"
| "no_permission"
| "unknown_url"
| "data_not_found"
| "unauthorized";
class HttpError extends Error { class HttpError extends Error {
/** /**
* HTTP Status Code * HTTP Status Code
*/ */
status: HttpStatus; status: HttpStatus;
message: string; message: string;
devMessage?: DevMessage;
constructor(status: HttpStatus, message: string) { constructor(status: HttpStatus, message: string, devMessage?: DevMessage) {
super(message); super(message);
this.name = "HttpError"; this.name = "HttpError";
this.status = status; this.status = status;
this.message = message; this.message = message;
this.devMessage = devMessage;
} }
} }

View file

@ -16,11 +16,7 @@ const jwtVerify = createVerifier({
const jwtDecode = createDecoder(); const jwtDecode = createDecoder();
export async function keycloakAuth( export async function keycloakAuth(request: Express.Request, roles?: string[]) {
request: Express.Request,
_securityName?: string,
_scopes?: string[],
) {
const token = request.headers["authorization"]?.includes("Bearer ") const token = request.headers["authorization"]?.includes("Bearer ")
? request.headers["authorization"].split(" ")[1] ? request.headers["authorization"].split(" ")[1]
: request.headers["authorization"]; : request.headers["authorization"];
@ -39,7 +35,7 @@ export async function keycloakAuth(
payload = await verifyOffline(token); payload = await verifyOffline(token);
break; break;
default: default:
if (process.env.KC_REALM_URL) { if (process.env.KC_URL) {
payload = await verifyOnline(token); payload = await verifyOnline(token);
break; break;
} }
@ -49,6 +45,12 @@ export async function keycloakAuth(
} }
} }
if (Array.isArray(payload.roles) && Array.isArray(roles) && roles.length > 0) {
if (!roles.some((a: string) => payload.roles.includes(a))) {
throw new HttpError(HttpStatus.FORBIDDEN, "คุณไม่มีสิทธิในการเข้าถึงข้อมูลดังกล่าว");
}
}
return payload; return payload;
} }

View file

@ -6,11 +6,11 @@ import { keycloakAuth } from "./auth-provider/keycloak";
export async function expressAuthentication( export async function expressAuthentication(
request: Express.Request, request: Express.Request,
securityName: string, securityName: string,
_scopes?: string[], scopes?: string[],
) { ) {
switch (securityName) { switch (securityName) {
case "keycloak": case "keycloak":
return keycloakAuth(request); return keycloakAuth(request, scopes);
default: default:
throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "ไม่ทราบวิธียืนยันตัวตน"); throw new HttpError(HttpStatus.NOT_IMPLEMENTED, "ไม่ทราบวิธียืนยันตัวตน");
} }

View file

@ -8,6 +8,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction)
return res.status(error.status).json({ return res.status(error.status).json({
status: error.status, status: error.status,
message: error.message, message: error.message,
devMessage: error.devMessage,
}); });
} }
@ -16,6 +17,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction)
status: HttpStatus.UNPROCESSABLE_ENTITY, status: HttpStatus.UNPROCESSABLE_ENTITY,
message: "Validation error(s).", message: "Validation error(s).",
detail: error.fields, detail: error.fields,
devMessage: "missing_or_invalid_parameter",
}); });
} }
@ -24,6 +26,7 @@ function error(error: Error, _req: Request, res: Response, _next: NextFunction)
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
status: HttpStatus.INTERNAL_SERVER_ERROR, status: HttpStatus.INTERNAL_SERVER_ERROR,
message: error.message, message: error.message,
devMessage: "system_error",
}); });
} }

79
src/middlewares/log.ts Normal file
View file

@ -0,0 +1,79 @@
import { NextFunction, Request, Response } from "express";
import elasticsearch from "../services/elasticsearch";
if (!process.env.ELASTICSEARCH_INDEX) {
throw new Error("Require ELASTICSEARCH_INDEX to store log.");
}
const ELASTICSEARCH_INDEX = process.env.ELASTICSEARCH_INDEX;
const LOG_LEVEL_MAP: Record<string, number> = {
debug: 4,
info: 3,
warning: 2,
error: 1,
none: 0,
};
async function logMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.url.startsWith("/api/")) return next();
let data: any;
const originalJson = res.json;
res.json = function (v: any) {
data = v;
return originalJson.call(this, v);
};
const timestamp = new Date().toString();
const start = performance.now();
req.app.locals.logData = {};
res.on("finish", () => {
if (!req.url.startsWith("/api/")) return;
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "info"] || 1;
if (level === 1 && res.statusCode < 500) return;
if (level === 2 && res.statusCode < 400) return;
if (level === 3 && res.statusCode < 200) return;
const obj = {
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
systemName: "JWS-SOS",
startTimeStamp: timestamp,
endTimeStamp: new Date().toString(),
processTime: performance.now() - start,
host: req.hostname,
sessionId: req.headers["x-session-id"],
rtId: req.headers["x-rtid"],
tId: req.headers["x-tid"],
method: req.method,
endpoint: req.url,
responseCode: res.statusCode,
responseDescription:
data?.devMessage !== undefined
? data.devMessage
: { 200: "success", 201: "created_success", 204: "no_content", 304: "success" }[
res.statusCode
],
input: (level === 4 && JSON.stringify(req.body, null, 2)) || undefined,
output: (level === 4 && JSON.stringify(data, null, 2)) || undefined,
...req.app.locals.logData,
};
console.log(obj);
elasticsearch.index({
index: ELASTICSEARCH_INDEX,
document: obj,
});
});
return next();
}
export default logMiddleware;

View file

@ -0,0 +1,7 @@
import { Client } from "@elastic/elasticsearch";
const elasticsearch = new Client({
node: `${process.env.ELASTICSEARCH_PROTOCOL}://${process.env.ELASTICSEARCH_HOST}:${process.env.ELASTICSEARCH_PORT}`,
});
export default elasticsearch;

View file

@ -91,6 +91,63 @@ export async function createUser(username: string, password: string, opts?: Reco
return id || true; return id || true;
} }
/**
* Update keycloak user by uuid
*
* Client must have permission to manage realm's user
*
* @returns user uuid or true if success, false otherwise.
*/
export async function editUser(userId: string, opts: Record<string, any>) {
const { password, ...rest } = opts;
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "PUT",
body: JSON.stringify({
enabled: true,
credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
...rest,
}),
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false;
if (!res.ok) {
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
}
const path = res.headers.get("Location");
const id = path?.split("/").at(-1);
return id || true;
}
/**
* Delete keycloak user by uuid
*
* Client must have permission to manage realm's user
*
* @returns user uuid or true if success, false otherwise.
*/
export async function deleteUser(userId: string) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "DELETE",
}).catch((e) => console.log("Keycloak Error: ", e));
if (!res) return false;
if (!res.ok) {
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
}
}
/** /**
* Get roles list or specific role data * Get roles list or specific role data
* *
@ -121,7 +178,46 @@ export async function getRoles(name?: string) {
const data = await res.json(); const data = await res.json();
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data.map((v: Record<string, any>) => ({ id: v.id, name: v.name })); return data.map((v: Record<string, string>) => ({ id: v.id, name: v.name }));
}
return {
id: data.id,
name: data.name,
};
}
/**
* Get roles list of user
*
* Client must have permission to get realms roles
*
* @returns role's info (array if not specify name) if success, null if not found, false otherwise.
*/
export async function getUserRoles(userId: string) {
const res = await fetch(
`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`,
{
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
},
},
).catch((e) => console.log(e));
if (!res) return false;
if (!res.ok && res.status !== 404) {
return Boolean(console.error("Keycloak Error Response: ", await res.json()));
}
if (res.status === 404) {
return null;
}
const data = await res.json();
if (Array.isArray(data)) {
return data.map((v: Record<string, string>) => ({ id: v.id, name: v.name }));
} }
return { return {
@ -137,7 +233,7 @@ export async function getRoles(name?: string) {
* *
* @returns true if success, false otherwise. * @returns true if success, false otherwise.
*/ */
export async function addUserRoles(userId: string, roleId: string[]) { export async function addUserRoles(userId: string, roles: { id: string; name: string }[]) {
const res = await fetch( const res = await fetch(
`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`, `${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`,
{ {
@ -147,7 +243,7 @@ export async function addUserRoles(userId: string, roleId: string[]) {
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "POST", method: "POST",
body: JSON.stringify(roleId.map((v) => ({ id: v }))), body: JSON.stringify(roles),
}, },
).catch((e) => console.log(e)); ).catch((e) => console.log(e));
@ -165,7 +261,7 @@ export async function addUserRoles(userId: string, roleId: string[]) {
* *
* @returns true if success, false otherwise. * @returns true if success, false otherwise.
*/ */
export async function removeUserRoles(userId: string, roleId: string[]) { export async function removeUserRoles(userId: string, roles: { id: string; name: string }[]) {
const res = await fetch( const res = await fetch(
`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`, `${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`,
{ {
@ -175,7 +271,7 @@ export async function removeUserRoles(userId: string, roleId: string[]) {
"content-type": `application/json`, "content-type": `application/json`,
}, },
method: "DELETE", method: "DELETE",
body: JSON.stringify(roleId.map((v) => ({ id: v }))), body: JSON.stringify(roles),
}, },
).catch((e) => console.log(e)); ).catch((e) => console.log(e));

View file

@ -12,6 +12,28 @@
"description": "Keycloak Bearer Token", "description": "Keycloak Bearer Token",
"in": "header" "in": "header"
} }
},
"spec": {
"tags": [
{ "name": "OpenAPI" },
{ "name": "Single-Sign On" },
{ "name": "Permission" },
{ "name": "Address" },
{ "name": "Branch" },
{ "name": "Branch Contact" },
{ "name": "User" },
{ "name": "Branch User" },
{ "name": "Customer" },
{ "name": "Customer Branch" },
{ "name": "Employee" },
{ "name": "Employee Checkup" },
{ "name": "Employee Work" },
{ "name": "Employee Other Info" },
{ "name": "Service" },
{ "name": "Work" },
{ "name": "Product Type" },
{ "name": "Product Group" }
]
} }
}, },
"routes": { "routes": {