feat: initial commit with seeded database and premium ui changes
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
PORT=4000
|
||||
JWT_SECRET=replace-with-secure-random-secret
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
DATABASE_URL=postgres://sig:sig_secret@localhost:5432/sig
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY src ./src
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
@@ -0,0 +1,218 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "sig-backend",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.13.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="],
|
||||
|
||||
"body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnsigP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||
|
||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||
|
||||
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["mime@1.6.0", "", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||
|
||||
"pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@7.8.3", "", { "bin": "bin/semver.js" }, "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg=="],
|
||||
|
||||
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
||||
|
||||
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
}
|
||||
}
|
||||
Generated
+940
@@ -0,0 +1,940 @@
|
||||
{
|
||||
"name": "sig-backend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sig-backend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.13.3"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.21.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.13.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
"pg-protocol": "^1.14.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.4.0",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.13.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.14.0",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.14.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.3",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "sig-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Auth backend for SIG dashboard",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "node --watch src/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.13.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const authRoutes = require("./routes/auth.routes");
|
||||
const householdsRoutes = require("./routes/households.routes");
|
||||
const poiRoutes = require("./routes/poi.routes");
|
||||
const landRoutes = require("./routes/land.routes");
|
||||
const gasStationRoutes = require("./routes/gas-station.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.use("/auth", authRoutes);
|
||||
app.use("/households", householdsRoutes);
|
||||
app.use("/poi", poiRoutes);
|
||||
app.use("/land", landRoutes);
|
||||
app.use("/gas-station", gasStationRoutes);
|
||||
|
||||
app.get("/health", (_req, res) => res.json({ status: "ok" }));
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,253 @@
|
||||
const bcrypt = require("bcryptjs");
|
||||
const pool = require("./db");
|
||||
const {
|
||||
SEED_HOUSEHOLDS,
|
||||
SEED_POI,
|
||||
SEED_LAND_MARKERS,
|
||||
SEED_LAND_SHAPES,
|
||||
SEED_GAS_STATIONS,
|
||||
} = require("./seed-data");
|
||||
|
||||
const SALT = 10;
|
||||
|
||||
const SEED_USERS = [
|
||||
{ username: "superadmin", email: "superadmin@app.local", role: "superadmin", password: "Super@1234" },
|
||||
{ username: "admin_poverty", email: "poverty@app.local", role: "admin_poverty", password: "Poverty@1234" },
|
||||
{ username: "admin_lands", email: "lands@app.local", role: "admin_lands_roads", password: "Lands@1234" },
|
||||
{ username: "admin_gas", email: "gas@app.local", role: "admin_gas_stations",password: "Gas@1234" },
|
||||
];
|
||||
|
||||
async function initDb() {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
role VARCHAR(100) NOT NULL,
|
||||
password_hash TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS poor_households (
|
||||
id SERIAL PRIMARY KEY,
|
||||
head_name VARCHAR(255) NOT NULL,
|
||||
family_count INT NOT NULL DEFAULT 1,
|
||||
poverty_level VARCHAR(10) NOT NULL DEFAULT 'Miskin',
|
||||
latitude DECIMAL(10, 8) NOT NULL,
|
||||
longitude DECIMAL(11, 8) NOT NULL,
|
||||
notes TEXT,
|
||||
penghasilan INT NOT NULL DEFAULT 0,
|
||||
kondisi_rumah SMALLINT NOT NULL DEFAULT 5,
|
||||
kepemilikan_aset SMALLINT NOT NULL DEFAULT 5,
|
||||
marker_type VARCHAR(50) DEFAULT 'marker',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS poi_markers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
poi_type VARCHAR(50) NOT NULL,
|
||||
religion_subtype VARCHAR(50),
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE poor_households
|
||||
ADD COLUMN IF NOT EXISTS fuzzy_score DECIMAL(5,2) DEFAULT NULL
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE poor_households
|
||||
ADD COLUMN IF NOT EXISTS fuzzy_label VARCHAR(50) DEFAULT NULL
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE poi_markers
|
||||
ADD COLUMN IF NOT EXISTS radius_meters INT DEFAULT 0
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS land_markers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
marker_type VARCHAR(50) NOT NULL,
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
notes TEXT,
|
||||
created_by INT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_land_markers_created_by ON land_markers(created_by)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS land_shapes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
shape_type VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50),
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
coordinates JSONB,
|
||||
radius_meters DECIMAL(10,2),
|
||||
notes TEXT,
|
||||
created_by INT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_land_shapes_created_by ON land_shapes(created_by)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS gas_station_markers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
marker_category VARCHAR(50) NOT NULL,
|
||||
brand VARCHAR(50),
|
||||
sub_type VARCHAR(100),
|
||||
gas_types JSONB,
|
||||
operating_hours VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
notes TEXT,
|
||||
created_by INT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_gas_station_markers_created_by ON gas_station_markers(created_by)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_gas_station_markers_category ON gas_station_markers(marker_category)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS pertamina_fuel_prices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
last_fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
raw_data JSONB NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// ── Seed Users ──────────────────────────────────────────────────────────────
|
||||
for (const u of SEED_USERS) {
|
||||
const hash = await bcrypt.hash(u.password, SALT);
|
||||
await pool.query(
|
||||
`INSERT INTO users (username, email, role, password_hash)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (username) DO NOTHING`,
|
||||
[u.username, u.email, u.role, hash]
|
||||
);
|
||||
}
|
||||
console.log(`[seed] users seeded (${SEED_USERS.length} entries)`);
|
||||
|
||||
// ── Seed Poor Households ────────────────────────────────────────────────────
|
||||
const hhCount = await pool.query("SELECT COUNT(*)::int AS c FROM poor_households");
|
||||
if (hhCount.rows[0].c === 0) {
|
||||
for (const h of SEED_HOUSEHOLDS) {
|
||||
await pool.query(
|
||||
`INSERT INTO poor_households
|
||||
(head_name, family_count, poverty_level, latitude, longitude, notes,
|
||||
penghasilan, kondisi_rumah, kepemilikan_aset, fuzzy_score, fuzzy_label)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
|
||||
[
|
||||
h.head_name, h.family_count, h.poverty_level,
|
||||
h.latitude, h.longitude, h.notes,
|
||||
h.penghasilan, h.kondisi_rumah, h.kepemilikan_aset,
|
||||
h.fuzzy_score, h.fuzzy_label,
|
||||
]
|
||||
);
|
||||
}
|
||||
console.log(`[seed] poor_households seeded (${SEED_HOUSEHOLDS.length} entries)`);
|
||||
}
|
||||
|
||||
// ── Seed POI Markers ────────────────────────────────────────────────────────
|
||||
const poiCount = await pool.query("SELECT COUNT(*)::int AS c FROM poi_markers");
|
||||
if (poiCount.rows[0].c === 0) {
|
||||
for (const p of SEED_POI) {
|
||||
await pool.query(
|
||||
`INSERT INTO poi_markers (name, poi_type, religion_subtype, latitude, longitude, notes, radius_meters)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
||||
[p.name, p.poi_type, p.religion_subtype, p.latitude, p.longitude, p.notes, p.radius_meters]
|
||||
);
|
||||
}
|
||||
console.log(`[seed] poi_markers seeded (${SEED_POI.length} entries)`);
|
||||
}
|
||||
|
||||
// ── Seed Land Markers ───────────────────────────────────────────────────────
|
||||
const lmCount = await pool.query("SELECT COUNT(*)::int AS c FROM land_markers");
|
||||
if (lmCount.rows[0].c === 0) {
|
||||
// Get superadmin id for created_by
|
||||
const adminRow = await pool.query("SELECT id FROM users WHERE username = 'superadmin' LIMIT 1");
|
||||
const createdBy = adminRow.rows.length ? adminRow.rows[0].id : null;
|
||||
|
||||
for (const lm of SEED_LAND_MARKERS) {
|
||||
await pool.query(
|
||||
`INSERT INTO land_markers (name, marker_type, latitude, longitude, notes, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)`,
|
||||
[lm.name, lm.marker_type, lm.latitude, lm.longitude, lm.notes, createdBy]
|
||||
);
|
||||
}
|
||||
console.log(`[seed] land_markers seeded (${SEED_LAND_MARKERS.length} entries)`);
|
||||
}
|
||||
|
||||
// ── Seed Land Shapes ────────────────────────────────────────────────────────
|
||||
const lsCount = await pool.query("SELECT COUNT(*)::int AS c FROM land_shapes");
|
||||
if (lsCount.rows[0].c === 0) {
|
||||
const adminRow2 = await pool.query("SELECT id FROM users WHERE username = 'superadmin' LIMIT 1");
|
||||
const createdBy2 = adminRow2.rows.length ? adminRow2.rows[0].id : null;
|
||||
|
||||
for (const ls of SEED_LAND_SHAPES) {
|
||||
await pool.query(
|
||||
`INSERT INTO land_shapes (name, shape_type, status, latitude, longitude, coordinates, radius_meters, notes, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
|
||||
[ls.name, ls.shape_type, ls.status, ls.latitude, ls.longitude, ls.coordinates, ls.radius_meters, ls.notes, createdBy2]
|
||||
);
|
||||
}
|
||||
console.log(`[seed] land_shapes seeded (${SEED_LAND_SHAPES.length} entries)`);
|
||||
}
|
||||
|
||||
// ── Seed Gas Station Markers ────────────────────────────────────────────────
|
||||
const gsCount = await pool.query("SELECT COUNT(*)::int AS c FROM gas_station_markers");
|
||||
if (gsCount.rows[0].c === 0) {
|
||||
const adminRow3 = await pool.query("SELECT id FROM users WHERE username = 'superadmin' LIMIT 1");
|
||||
const createdBy3 = adminRow3.rows.length ? adminRow3.rows[0].id : null;
|
||||
|
||||
for (const gs of SEED_GAS_STATIONS) {
|
||||
await pool.query(
|
||||
`INSERT INTO gas_station_markers
|
||||
(name, marker_category, brand, sub_type, gas_types, operating_hours, status, latitude, longitude, notes, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
|
||||
[
|
||||
gs.name, gs.marker_category, gs.brand, gs.sub_type,
|
||||
gs.gas_types, gs.operating_hours, gs.status,
|
||||
gs.latitude, gs.longitude, gs.notes, createdBy3,
|
||||
]
|
||||
);
|
||||
}
|
||||
console.log(`[seed] gas_station_markers seeded (${SEED_GAS_STATIONS.length} entries)`);
|
||||
}
|
||||
|
||||
console.log("[seed] Database initialization complete.");
|
||||
}
|
||||
|
||||
module.exports = initDb;
|
||||
@@ -0,0 +1,7 @@
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
module.exports = pool;
|
||||
@@ -0,0 +1,71 @@
|
||||
const express = require("express");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { findByUsernameOrEmail } = require("../users");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
||||
const JWT_EXPIRES_IN = "8h";
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
const username = req.body.username?.trim();
|
||||
const password = req.body.password;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: "username and password are required" });
|
||||
}
|
||||
|
||||
const user = await findByUsernameOrEmail(username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password_hash);
|
||||
if (!match) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
|
||||
return res.json({
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/me", (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
return res.json({
|
||||
user: {
|
||||
id: payload.sub,
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,339 @@
|
||||
const express = require("express");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const pool = require("../db");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
||||
|
||||
// Parser helper
|
||||
function parseFuelData(apiResponse) {
|
||||
if (!apiResponse || !apiResponse.data || !apiResponse.data.content) {
|
||||
throw new Error("Invalid API response structure");
|
||||
}
|
||||
|
||||
const content = apiResponse.data.content;
|
||||
const productTableNode = Object.values(content).find(
|
||||
node => node && node.type && node.type.resolvedName === "ProductTable"
|
||||
);
|
||||
|
||||
if (!productTableNode || !productTableNode.props || !Array.isArray(productTableNode.props.items)) {
|
||||
throw new Error("ProductTable not found in content");
|
||||
}
|
||||
|
||||
const items = productTableNode.props.items;
|
||||
|
||||
const headingNode = Object.values(content).find(
|
||||
node => node && node.type && node.type.resolvedName === "Heading"
|
||||
);
|
||||
const lastUpdated = headingNode?.props?.text || apiResponse.data.title || "Update terbaru";
|
||||
|
||||
const gasoline = [];
|
||||
const diesel = [];
|
||||
|
||||
items.forEach(item => {
|
||||
const isGasoline = item.title && (item.title.toLowerCase() === "gasoline" || item.title.toLowerCase() === "bensin");
|
||||
const list = isGasoline ? gasoline : diesel;
|
||||
|
||||
if (Array.isArray(item.data)) {
|
||||
item.data.forEach(row => {
|
||||
const parsed = {
|
||||
region: row.REGION?.trim(),
|
||||
pertalite: null,
|
||||
pertamax: null,
|
||||
pertamaxGreen: null,
|
||||
pertamaxTurbo: null,
|
||||
pertamaxPertashop: null,
|
||||
pertaminaDex: null,
|
||||
dexlite: null,
|
||||
bioSolarNonSubsidi: null,
|
||||
bioSolarSubsidi: null
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (key === "REGION") continue;
|
||||
|
||||
const cleanVal = value?.trim().replace(/,/g, "");
|
||||
const numericVal = (cleanVal && cleanVal !== "-" && !isNaN(cleanVal)) ? parseInt(cleanVal, 10) : null;
|
||||
|
||||
if (key.includes("product-table-pertalite.png")) {
|
||||
parsed.pertalite = numericVal;
|
||||
} else if (key.includes("product-table-pertamax.png")) {
|
||||
parsed.pertamax = numericVal;
|
||||
} else if (key.includes("product-table-pertamax-green-95.png")) {
|
||||
parsed.pertamaxGreen = numericVal;
|
||||
} else if (key.includes("product-table-pertamax-turbo.png")) {
|
||||
parsed.pertamaxTurbo = numericVal;
|
||||
} else if (key.includes("harga-produk-pertamax-pertashop.jpg")) {
|
||||
parsed.pertamaxPertashop = numericVal;
|
||||
} else if (key.includes("product-table-pertamina-dex.png")) {
|
||||
parsed.pertaminaDex = numericVal;
|
||||
} else if (key.includes("product-table-dexlite.png")) {
|
||||
parsed.dexlite = numericVal;
|
||||
} else if (key.includes("harga-produk-bio-solar-non-subsidi.jpg")) {
|
||||
parsed.bioSolarNonSubsidi = numericVal;
|
||||
} else if (key.includes("harga-produk-bio-solar-subsidi.jpg")) {
|
||||
parsed.bioSolarSubsidi = numericVal;
|
||||
}
|
||||
}
|
||||
|
||||
list.push(parsed);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
succeeded: true,
|
||||
lastUpdated,
|
||||
gasoline,
|
||||
diesel
|
||||
};
|
||||
}
|
||||
|
||||
// GET latest Pertamina fuel prices (cached, daily fetch)
|
||||
router.get("/fuel-prices", async (_req, res) => {
|
||||
try {
|
||||
// 1. Check if we have a fresh DB cache
|
||||
const { rows } = await pool.query(
|
||||
"SELECT * FROM pertamina_fuel_prices ORDER BY last_fetched_at DESC LIMIT 1"
|
||||
);
|
||||
|
||||
let rawData = null;
|
||||
let shouldFetch = true;
|
||||
|
||||
if (rows.length > 0) {
|
||||
const cache = rows[0];
|
||||
const now = new Date();
|
||||
const lastFetched = new Date(cache.last_fetched_at);
|
||||
const diffMs = now - lastFetched;
|
||||
|
||||
// If cached less than 24 hours ago, use cache
|
||||
if (diffMs < 24 * 60 * 60 * 1000) {
|
||||
rawData = cache.raw_data;
|
||||
shouldFetch = false;
|
||||
console.log("Serving fuel prices from DB cache");
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFetch) {
|
||||
console.log("DB Cache stale or missing. Fetching fresh fuel prices from Pertamina API...");
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://pertaminapatraniaga.com/api/api/v1/post/get-by-slug/page/harga-terbaru-bbm?language=id"
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
rawData = await response.json();
|
||||
|
||||
// Save to DB cache
|
||||
await pool.query(
|
||||
"INSERT INTO pertamina_fuel_prices (last_fetched_at, raw_data) VALUES (NOW(), $1)",
|
||||
[JSON.stringify(rawData)]
|
||||
);
|
||||
console.log("Successfully cached fresh fuel prices to DB");
|
||||
} catch (fetchErr) {
|
||||
console.error("Failed to fetch fresh fuel prices from Pertamina API:", fetchErr);
|
||||
// Fall back to stale cache from DB if available
|
||||
if (rows.length > 0) {
|
||||
rawData = rows[0].raw_data;
|
||||
console.warn("Serving stale fuel prices from DB cache after fetch failure");
|
||||
} else {
|
||||
// If no DB cache exists, read from local fallback file
|
||||
console.warn("No DB cache available. Loading from pertamina-fallback.json file");
|
||||
const fallbackPath = path.join(__dirname, "pertamina-fallback.json");
|
||||
const fileData = fs.readFileSync(fallbackPath, "utf8");
|
||||
rawData = JSON.parse(fileData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseFuelData(rawData);
|
||||
res.json(parsed);
|
||||
} catch (err) {
|
||||
console.error("Error in /fuel-prices endpoint:", err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(authHeader.slice(7), JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
// GET all gas station markers
|
||||
router.get("/markers", async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT gsm.*, u.username AS created_by_username
|
||||
FROM gas_station_markers gsm
|
||||
LEFT JOIN users u ON u.id = gsm.created_by
|
||||
ORDER BY gsm.created_at DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET stats
|
||||
router.get("/stats", async (_req, res) => {
|
||||
try {
|
||||
const totalResult = await pool.query("SELECT COUNT(*) FROM gas_station_markers");
|
||||
const categoryResult = await pool.query(`
|
||||
SELECT marker_category, COUNT(*) as count
|
||||
FROM gas_station_markers
|
||||
GROUP BY marker_category
|
||||
`);
|
||||
const activeEvResult = await pool.query(`
|
||||
SELECT COUNT(*) FROM gas_station_markers
|
||||
WHERE marker_category = 'charging-station' AND status = 'Active'
|
||||
`);
|
||||
|
||||
const stats = {
|
||||
total: parseInt(totalResult.rows[0].count, 10),
|
||||
gas_pump: 0,
|
||||
charging_station: 0,
|
||||
charging_station_active: parseInt(activeEvResult.rows[0].count, 10),
|
||||
wrench: 0
|
||||
};
|
||||
|
||||
categoryResult.rows.forEach(r => {
|
||||
const cat = r.marker_category;
|
||||
if (cat === "gas-pump") stats.gas_pump = parseInt(r.count, 10);
|
||||
else if (cat === "charging-station") stats.charging_station = parseInt(r.count, 10);
|
||||
else if (cat === "wrench") stats.wrench = parseInt(r.count, 10);
|
||||
});
|
||||
|
||||
res.json(stats);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET single marker
|
||||
router.get("/markers/:id", async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT gsm.*, u.username AS created_by_username
|
||||
FROM gas_station_markers gsm
|
||||
LEFT JOIN users u ON u.id = gsm.created_by
|
||||
WHERE gsm.id = $1
|
||||
`, [req.params.id]);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST new marker
|
||||
router.post("/markers", requireAuth, async (req, res) => {
|
||||
const { name, marker_category, brand, sub_type, gas_types, operating_hours, status, latitude, longitude, notes } = req.body;
|
||||
|
||||
if (!name || !marker_category || latitude == null || longitude == null) {
|
||||
return res.status(400).json({ error: "name, marker_category, latitude, longitude are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO gas_station_markers (
|
||||
name, marker_category, brand, sub_type, gas_types, operating_hours, status, latitude, longitude, notes, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
marker_category,
|
||||
brand ?? null,
|
||||
sub_type ?? null,
|
||||
gas_types ? JSON.stringify(gas_types) : null,
|
||||
operating_hours ?? null,
|
||||
status ?? null,
|
||||
latitude,
|
||||
longitude,
|
||||
notes ?? null,
|
||||
req.user.sub
|
||||
]
|
||||
);
|
||||
res.status(201).json({ ...rows[0], created_by_username: req.user.username });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update marker
|
||||
router.put("/markers/:id", requireAuth, async (req, res) => {
|
||||
const { name, marker_category, brand, sub_type, gas_types, operating_hours, status, latitude, longitude, notes } = req.body;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
WITH updated AS (
|
||||
UPDATE gas_station_markers
|
||||
SET name = COALESCE($1, name),
|
||||
marker_category = COALESCE($2, marker_category),
|
||||
brand = $3,
|
||||
sub_type = $4,
|
||||
gas_types = $5,
|
||||
operating_hours = $6,
|
||||
status = $7,
|
||||
latitude = COALESCE($8, latitude),
|
||||
longitude = COALESCE($9, longitude),
|
||||
notes = $10,
|
||||
updated_at = NOW()
|
||||
WHERE id = $11
|
||||
RETURNING *
|
||||
)
|
||||
SELECT updated.*, u.username AS created_by_username
|
||||
FROM updated
|
||||
LEFT JOIN users u ON u.id = updated.created_by
|
||||
`, [
|
||||
name ?? null,
|
||||
marker_category ?? null,
|
||||
brand ?? null,
|
||||
sub_type ?? null,
|
||||
gas_types ? JSON.stringify(gas_types) : null,
|
||||
operating_hours ?? null,
|
||||
status ?? null,
|
||||
latitude ?? null,
|
||||
longitude ?? null,
|
||||
notes ?? null,
|
||||
req.params.id
|
||||
]);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE marker
|
||||
router.delete("/markers/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
"DELETE FROM gas_station_markers WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rowCount) return res.status(404).json({ error: "Not found" });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,311 @@
|
||||
const express = require("express");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const pool = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
||||
const FUZZY_URL = process.env.FUZZY_SERVICE_URL ?? "http://localhost:5001";
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(authHeader.slice(7), JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
const FUZZY_LABEL_TO_POVERTY_LEVEL = {
|
||||
"SANGAT TINGGI": "Extreme",
|
||||
"TINGGI": "Extreme",
|
||||
"SEDANG": "Miskin",
|
||||
"RENDAH": "Miskin",
|
||||
"TIDAK PRIORITAS": "Rentan",
|
||||
};
|
||||
|
||||
async function callFuzzyService(id, penghasilan, tanggungan, kondisi_rumah, kepemilikan_aset) {
|
||||
try {
|
||||
const res = await fetch(`${FUZZY_URL}/compute`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, penghasilan, tanggungan, kondisi_rumah, kepemilikan_aset }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return await res.json(); // { id, score, label }
|
||||
} catch {
|
||||
return null; // degrade gracefully when service is unavailable
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/stats-fuzzy", async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT fuzzy_label, COUNT(*) AS count
|
||||
FROM poor_households
|
||||
WHERE fuzzy_label IS NOT NULL
|
||||
GROUP BY fuzzy_label
|
||||
`);
|
||||
const labels = ["SANGAT TINGGI", "TINGGI", "SEDANG", "RENDAH", "TIDAK PRIORITAS"];
|
||||
const stats = Object.fromEntries(labels.map((l) => [l, 0]));
|
||||
for (const row of rows) stats[row.fuzzy_label] = parseInt(row.count, 10);
|
||||
res.json(stats);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregated, DB-backed numbers for the poverty dashboard.
|
||||
router.get("/stats-overview", async (_req, res) => {
|
||||
try {
|
||||
const [totalsQ, labelsQ, monthlyQ, poiQ] = await Promise.all([
|
||||
pool.query(`
|
||||
SELECT COUNT(*)::int AS household_count,
|
||||
COALESCE(SUM(family_count),0)::int AS total_dependents,
|
||||
COALESCE(AVG(penghasilan),0)::float AS avg_income,
|
||||
AVG(fuzzy_score)::float AS avg_score
|
||||
FROM poor_households
|
||||
`),
|
||||
pool.query(`
|
||||
SELECT fuzzy_label, COUNT(*)::int AS count
|
||||
FROM poor_households
|
||||
WHERE fuzzy_label IS NOT NULL
|
||||
GROUP BY fuzzy_label
|
||||
`),
|
||||
pool.query(`
|
||||
SELECT to_char(date_trunc('month', created_at), 'YYYY-MM') AS month,
|
||||
COUNT(*)::int AS count,
|
||||
AVG(fuzzy_score)::float AS avg_score
|
||||
FROM poor_households
|
||||
WHERE created_at >= date_trunc('month', NOW()) - INTERVAL '11 months'
|
||||
GROUP BY 1
|
||||
ORDER BY 1
|
||||
`),
|
||||
pool.query(`
|
||||
SELECT poi_type, COUNT(*)::int AS count
|
||||
FROM poi_markers
|
||||
GROUP BY poi_type
|
||||
`),
|
||||
]);
|
||||
|
||||
const labels = ["SANGAT TINGGI", "TINGGI", "SEDANG", "RENDAH", "TIDAK PRIORITAS"];
|
||||
const byLabel = Object.fromEntries(labels.map((l) => [l, 0]));
|
||||
for (const row of labelsQ.rows) byLabel[row.fuzzy_label] = row.count;
|
||||
|
||||
res.json({
|
||||
totals: totalsQ.rows[0],
|
||||
by_label: byLabel,
|
||||
monthly: monthlyQ.rows,
|
||||
poi: Object.fromEntries(poiQ.rows.map((r) => [r.poi_type, r.count])),
|
||||
});
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Re-score every stored household with the current fuzzy engine.
|
||||
router.post("/recompute-fuzzy", requireAuth, async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, penghasilan, family_count, kondisi_rumah, kepemilikan_aset
|
||||
FROM poor_households`
|
||||
);
|
||||
if (!rows.length) return res.json({ updated: 0, total: 0 });
|
||||
|
||||
const fuzzyRes = await fetch(`${FUZZY_URL}/batch`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
households: rows.map((r) => ({
|
||||
id: r.id,
|
||||
penghasilan: r.penghasilan,
|
||||
tanggungan: r.family_count,
|
||||
kondisi_rumah: r.kondisi_rumah,
|
||||
kepemilikan_aset: r.kepemilikan_aset,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
if (!fuzzyRes.ok) {
|
||||
return res.status(502).json({ error: "Fuzzy service unavailable" });
|
||||
}
|
||||
const { results } = await fuzzyRes.json();
|
||||
|
||||
let updated = 0;
|
||||
for (const r of results) {
|
||||
const povertyLevel = FUZZY_LABEL_TO_POVERTY_LEVEL[r.label];
|
||||
const { rowCount } = await pool.query(
|
||||
`UPDATE poor_households
|
||||
SET fuzzy_score = $1, fuzzy_label = $2,
|
||||
poverty_level = COALESCE($3, poverty_level)
|
||||
WHERE id = $4`,
|
||||
[r.score, r.label, povertyLevel ?? null, r.id]
|
||||
);
|
||||
updated += rowCount;
|
||||
}
|
||||
res.json({ updated, total: rows.length });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Recompute failed" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/", async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
"SELECT * FROM poor_households ORDER BY created_at DESC"
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const {
|
||||
head_name,
|
||||
family_count,
|
||||
latitude,
|
||||
longitude,
|
||||
notes,
|
||||
penghasilan,
|
||||
kondisi_rumah,
|
||||
kepemilikan_aset,
|
||||
marker_type,
|
||||
} = req.body;
|
||||
|
||||
if (!head_name || latitude == null || longitude == null) {
|
||||
return res.status(400).json({ error: "head_name, latitude, longitude are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO poor_households
|
||||
(head_name, family_count, poverty_level, latitude, longitude,
|
||||
notes, penghasilan, kondisi_rumah, kepemilikan_aset, marker_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *`,
|
||||
[
|
||||
head_name,
|
||||
family_count ?? 1,
|
||||
"Miskin",
|
||||
latitude,
|
||||
longitude,
|
||||
notes ?? null,
|
||||
penghasilan ?? 0,
|
||||
kondisi_rumah ?? 5,
|
||||
kepemilikan_aset ?? 5,
|
||||
marker_type ?? "marker",
|
||||
]
|
||||
);
|
||||
|
||||
const household = rows[0];
|
||||
|
||||
const fuzzyResult = await callFuzzyService(
|
||||
household.id,
|
||||
household.penghasilan,
|
||||
household.family_count, // family_count = tanggungan
|
||||
household.kondisi_rumah,
|
||||
household.kepemilikan_aset
|
||||
);
|
||||
|
||||
if (fuzzyResult) {
|
||||
const povertyLevel = FUZZY_LABEL_TO_POVERTY_LEVEL[fuzzyResult.label] ?? household.poverty_level;
|
||||
await pool.query(
|
||||
`UPDATE poor_households SET fuzzy_score = $1, fuzzy_label = $2, poverty_level = $3 WHERE id = $4`,
|
||||
[fuzzyResult.score, fuzzyResult.label, povertyLevel, household.id]
|
||||
);
|
||||
household.fuzzy_score = fuzzyResult.score;
|
||||
household.fuzzy_label = fuzzyResult.label;
|
||||
household.fuzzy_detail = fuzzyResult.faktor ?? null; // not persisted, response only
|
||||
household.poverty_level = povertyLevel;
|
||||
}
|
||||
|
||||
res.status(201).json(household);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", requireAuth, async (req, res) => {
|
||||
const {
|
||||
head_name,
|
||||
family_count,
|
||||
latitude,
|
||||
longitude,
|
||||
notes,
|
||||
penghasilan,
|
||||
kondisi_rumah,
|
||||
kepemilikan_aset,
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE poor_households
|
||||
SET head_name = COALESCE($1, head_name),
|
||||
family_count = COALESCE($2, family_count),
|
||||
latitude = COALESCE($3, latitude),
|
||||
longitude = COALESCE($4, longitude),
|
||||
notes = $5,
|
||||
penghasilan = COALESCE($6, penghasilan),
|
||||
kondisi_rumah = COALESCE($7, kondisi_rumah),
|
||||
kepemilikan_aset= COALESCE($8, kepemilikan_aset)
|
||||
WHERE id = $9
|
||||
RETURNING *`,
|
||||
[
|
||||
head_name ?? null,
|
||||
family_count ?? null,
|
||||
latitude ?? null,
|
||||
longitude ?? null,
|
||||
notes ?? null,
|
||||
penghasilan ?? null,
|
||||
kondisi_rumah ?? null,
|
||||
kepemilikan_aset ?? null,
|
||||
req.params.id,
|
||||
]
|
||||
);
|
||||
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
|
||||
const household = rows[0];
|
||||
const fuzzyResult = await callFuzzyService(
|
||||
household.id,
|
||||
household.penghasilan,
|
||||
household.family_count,
|
||||
household.kondisi_rumah,
|
||||
household.kepemilikan_aset
|
||||
);
|
||||
|
||||
if (fuzzyResult) {
|
||||
const povertyLevel = FUZZY_LABEL_TO_POVERTY_LEVEL[fuzzyResult.label] ?? household.poverty_level;
|
||||
await pool.query(
|
||||
`UPDATE poor_households SET fuzzy_score = $1, fuzzy_label = $2, poverty_level = $3 WHERE id = $4`,
|
||||
[fuzzyResult.score, fuzzyResult.label, povertyLevel, household.id]
|
||||
);
|
||||
household.fuzzy_score = fuzzyResult.score;
|
||||
household.fuzzy_label = fuzzyResult.label;
|
||||
household.fuzzy_detail = fuzzyResult.faktor ?? null; // not persisted, response only
|
||||
household.poverty_level = povertyLevel;
|
||||
}
|
||||
|
||||
res.json(household);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
"DELETE FROM poor_households WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rowCount) return res.status(404).json({ error: "Not found" });
|
||||
res.status(204).send();
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,233 @@
|
||||
const express = require("express");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const pool = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(authHeader.slice(7), JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
/* ───────── land_markers (marker / flag / protected / registry) ───────── */
|
||||
|
||||
router.get("/markers", async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT lm.*, u.username AS created_by_username
|
||||
FROM land_markers lm
|
||||
LEFT JOIN users u ON u.id = lm.created_by
|
||||
ORDER BY lm.created_at DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/markers/:id", async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT lm.*, u.username AS created_by_username
|
||||
FROM land_markers lm
|
||||
LEFT JOIN users u ON u.id = lm.created_by
|
||||
WHERE lm.id = $1
|
||||
`, [req.params.id]);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/markers", requireAuth, async (req, res) => {
|
||||
const { name, marker_type, latitude, longitude, notes } = req.body;
|
||||
|
||||
if (!name || !marker_type || latitude == null || longitude == null) {
|
||||
return res.status(400).json({ error: "name, marker_type, latitude, longitude are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO land_markers (name, marker_type, latitude, longitude, notes, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[name, marker_type, latitude, longitude, notes ?? null, req.user.sub]
|
||||
);
|
||||
res.status(201).json({ ...rows[0], created_by_username: req.user.username });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/markers/:id", requireAuth, async (req, res) => {
|
||||
const { name, marker_type, latitude, longitude, notes } = req.body;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
WITH updated AS (
|
||||
UPDATE land_markers
|
||||
SET name = COALESCE($1, name),
|
||||
marker_type = COALESCE($2, marker_type),
|
||||
latitude = COALESCE($3, latitude),
|
||||
longitude = COALESCE($4, longitude),
|
||||
notes = $5,
|
||||
updated_at = NOW()
|
||||
WHERE id = $6
|
||||
RETURNING *
|
||||
)
|
||||
SELECT updated.*, u.username AS created_by_username
|
||||
FROM updated
|
||||
LEFT JOIN users u ON u.id = updated.created_by
|
||||
`, [
|
||||
name ?? null,
|
||||
marker_type ?? null,
|
||||
latitude ?? null,
|
||||
longitude ?? null,
|
||||
notes ?? null,
|
||||
req.params.id
|
||||
]);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/markers/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
"DELETE FROM land_markers WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rowCount) return res.status(404).json({ error: "Not found" });
|
||||
res.status(204).send();
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
/* ───────── land_shapes (line / polygon / circle) ───────── */
|
||||
|
||||
router.get("/shapes", async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT ls.*, u.username AS created_by_username
|
||||
FROM land_shapes ls
|
||||
LEFT JOIN users u ON u.id = ls.created_by
|
||||
ORDER BY ls.created_at DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/shapes/:id", async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT ls.*, u.username AS created_by_username
|
||||
FROM land_shapes ls
|
||||
LEFT JOIN users u ON u.id = ls.created_by
|
||||
WHERE ls.id = $1
|
||||
`, [req.params.id]);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/shapes", requireAuth, async (req, res) => {
|
||||
const { name, shape_type, status, latitude, longitude, coordinates, radius_meters, notes } = req.body;
|
||||
|
||||
if (!name || !shape_type || latitude == null || longitude == null) {
|
||||
return res.status(400).json({ error: "name, shape_type, latitude, longitude are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO land_shapes (name, shape_type, status, latitude, longitude, coordinates, radius_meters, notes, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
shape_type,
|
||||
status ?? null,
|
||||
latitude,
|
||||
longitude,
|
||||
coordinates ? JSON.stringify(coordinates) : null,
|
||||
radius_meters ?? null,
|
||||
notes ?? null,
|
||||
req.user.sub
|
||||
]
|
||||
);
|
||||
res.status(201).json({ ...rows[0], created_by_username: req.user.username });
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/shapes/:id", requireAuth, async (req, res) => {
|
||||
const { name, shape_type, status, latitude, longitude, coordinates, radius_meters, notes } = req.body;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
WITH updated AS (
|
||||
UPDATE land_shapes
|
||||
SET name = COALESCE($1, name),
|
||||
shape_type = COALESCE($2, shape_type),
|
||||
status = $3,
|
||||
latitude = COALESCE($4, latitude),
|
||||
longitude = COALESCE($5, longitude),
|
||||
coordinates = COALESCE($6, coordinates),
|
||||
radius_meters = COALESCE($7, radius_meters),
|
||||
notes = $8,
|
||||
updated_at = NOW()
|
||||
WHERE id = $9
|
||||
RETURNING *
|
||||
)
|
||||
SELECT updated.*, u.username AS created_by_username
|
||||
FROM updated
|
||||
LEFT JOIN users u ON u.id = updated.created_by
|
||||
`, [
|
||||
name ?? null,
|
||||
shape_type ?? null,
|
||||
status ?? null,
|
||||
latitude ?? null,
|
||||
longitude ?? null,
|
||||
coordinates ? JSON.stringify(coordinates) : null,
|
||||
radius_meters ?? null,
|
||||
notes ?? null,
|
||||
req.params.id
|
||||
]);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/shapes/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
"DELETE FROM land_shapes WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rowCount) return res.status(404).json({ error: "Not found" });
|
||||
res.status(204).send();
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,112 @@
|
||||
const express = require("express");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const pool = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production";
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(authHeader.slice(7), JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/", async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
"SELECT * FROM poi_markers ORDER BY created_at DESC"
|
||||
);
|
||||
res.json(rows);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
"SELECT * FROM poi_markers WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { name, poi_type, religion_subtype, latitude, longitude, notes, radius_meters } = req.body;
|
||||
|
||||
if (!name || !poi_type || latitude == null || longitude == null) {
|
||||
return res.status(400).json({ error: "name, poi_type, latitude, longitude are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO poi_markers (name, poi_type, religion_subtype, latitude, longitude, notes, radius_meters)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[name, poi_type, religion_subtype ?? null, latitude, longitude, notes ?? null, radius_meters ?? 0]
|
||||
);
|
||||
res.status(201).json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", requireAuth, async (req, res) => {
|
||||
const { name, poi_type, religion_subtype, latitude, longitude, notes, radius_meters } = req.body;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE poi_markers
|
||||
SET name = COALESCE($1, name),
|
||||
poi_type = COALESCE($2, poi_type),
|
||||
religion_subtype = $3,
|
||||
latitude = COALESCE($4, latitude),
|
||||
longitude = COALESCE($5, longitude),
|
||||
notes = $6,
|
||||
radius_meters = COALESCE($7, radius_meters),
|
||||
updated_at = NOW()
|
||||
WHERE id = $8
|
||||
RETURNING *`,
|
||||
[
|
||||
name ?? null,
|
||||
poi_type ?? null,
|
||||
religion_subtype ?? null,
|
||||
latitude ?? null,
|
||||
longitude ?? null,
|
||||
notes ?? null,
|
||||
radius_meters ?? null,
|
||||
req.params.id
|
||||
]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: "Not found" });
|
||||
res.json(rows[0]);
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { rowCount } = await pool.query(
|
||||
"DELETE FROM poi_markers WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rowCount) return res.status(404).json({ error: "Not found" });
|
||||
res.status(204).send();
|
||||
} catch {
|
||||
res.status(500).json({ error: "Database error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Seed data for all dashboard tables.
|
||||
* All coordinates are in the Pontianak / Kalimantan Barat area.
|
||||
*/
|
||||
|
||||
// ─── Poverty Households (poor_households) ────────────────────────────────────
|
||||
const SEED_HOUSEHOLDS = [
|
||||
{
|
||||
head_name: "Budi Santoso",
|
||||
family_count: 5,
|
||||
poverty_level: "Extreme",
|
||||
latitude: -0.0263,
|
||||
longitude: 109.3425,
|
||||
notes: "Rumah semi-permanen, atap bocor",
|
||||
penghasilan: 800000,
|
||||
kondisi_rumah: 2,
|
||||
kepemilikan_aset: 1,
|
||||
fuzzy_score: 92.5,
|
||||
fuzzy_label: "SANGAT TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Siti Aminah",
|
||||
family_count: 4,
|
||||
poverty_level: "Extreme",
|
||||
latitude: -0.0301,
|
||||
longitude: 109.3388,
|
||||
notes: "Janda, 3 anak sekolah",
|
||||
penghasilan: 600000,
|
||||
kondisi_rumah: 2,
|
||||
kepemilikan_aset: 2,
|
||||
fuzzy_score: 88.3,
|
||||
fuzzy_label: "SANGAT TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Ahmad Hidayat",
|
||||
family_count: 6,
|
||||
poverty_level: "Miskin",
|
||||
latitude: -0.0195,
|
||||
longitude: 109.3501,
|
||||
notes: "Buruh harian lepas",
|
||||
penghasilan: 1200000,
|
||||
kondisi_rumah: 3,
|
||||
kepemilikan_aset: 3,
|
||||
fuzzy_score: 72.1,
|
||||
fuzzy_label: "TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Dewi Lestari",
|
||||
family_count: 3,
|
||||
poverty_level: "Miskin",
|
||||
latitude: -0.0352,
|
||||
longitude: 109.3290,
|
||||
notes: "Pedagang kecil di pasar",
|
||||
penghasilan: 1500000,
|
||||
kondisi_rumah: 4,
|
||||
kepemilikan_aset: 3,
|
||||
fuzzy_score: 65.0,
|
||||
fuzzy_label: "TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Rahmat Kurniawan",
|
||||
family_count: 7,
|
||||
poverty_level: "Extreme",
|
||||
latitude: -0.0410,
|
||||
longitude: 109.3460,
|
||||
notes: "Tinggal di bantaran sungai",
|
||||
penghasilan: 500000,
|
||||
kondisi_rumah: 1,
|
||||
kepemilikan_aset: 1,
|
||||
fuzzy_score: 95.8,
|
||||
fuzzy_label: "SANGAT TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Nurul Hasanah",
|
||||
family_count: 2,
|
||||
poverty_level: "Miskin",
|
||||
latitude: -0.0180,
|
||||
longitude: 109.3550,
|
||||
notes: "Ibu rumah tangga, suami sakit",
|
||||
penghasilan: 900000,
|
||||
kondisi_rumah: 3,
|
||||
kepemilikan_aset: 2,
|
||||
fuzzy_score: 78.4,
|
||||
fuzzy_label: "TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Hasan Basri",
|
||||
family_count: 4,
|
||||
poverty_level: "Rentan",
|
||||
latitude: -0.0225,
|
||||
longitude: 109.3610,
|
||||
notes: "Tukang ojek online",
|
||||
penghasilan: 2000000,
|
||||
kondisi_rumah: 5,
|
||||
kepemilikan_aset: 4,
|
||||
fuzzy_score: 45.2,
|
||||
fuzzy_label: "SEDANG",
|
||||
},
|
||||
{
|
||||
head_name: "Fatimah Zahra",
|
||||
family_count: 3,
|
||||
poverty_level: "Rentan",
|
||||
latitude: -0.0290,
|
||||
longitude: 109.3320,
|
||||
notes: "Penjahit rumahan",
|
||||
penghasilan: 1800000,
|
||||
kondisi_rumah: 5,
|
||||
kepemilikan_aset: 5,
|
||||
fuzzy_score: 38.6,
|
||||
fuzzy_label: "RENDAH",
|
||||
},
|
||||
{
|
||||
head_name: "Eko Prasetyo",
|
||||
family_count: 5,
|
||||
poverty_level: "Miskin",
|
||||
latitude: -0.0150,
|
||||
longitude: 109.3480,
|
||||
notes: "Nelayan kecil, pendapatan tidak tetap",
|
||||
penghasilan: 1100000,
|
||||
kondisi_rumah: 3,
|
||||
kepemilikan_aset: 2,
|
||||
fuzzy_score: 70.9,
|
||||
fuzzy_label: "TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Yanti Suryani",
|
||||
family_count: 2,
|
||||
poverty_level: "Rentan",
|
||||
latitude: -0.0330,
|
||||
longitude: 109.3570,
|
||||
notes: "Pekerja warung makan",
|
||||
penghasilan: 2200000,
|
||||
kondisi_rumah: 6,
|
||||
kepemilikan_aset: 5,
|
||||
fuzzy_score: 28.4,
|
||||
fuzzy_label: "RENDAH",
|
||||
},
|
||||
{
|
||||
head_name: "Muhammad Rizki",
|
||||
family_count: 8,
|
||||
poverty_level: "Extreme",
|
||||
latitude: -0.0380,
|
||||
longitude: 109.3400,
|
||||
notes: "Pengangguran, 7 tanggungan",
|
||||
penghasilan: 400000,
|
||||
kondisi_rumah: 1,
|
||||
kepemilikan_aset: 1,
|
||||
fuzzy_score: 97.2,
|
||||
fuzzy_label: "SANGAT TINGGI",
|
||||
},
|
||||
{
|
||||
head_name: "Sri Wahyuni",
|
||||
family_count: 3,
|
||||
poverty_level: "Rentan",
|
||||
latitude: -0.0205,
|
||||
longitude: 109.3650,
|
||||
notes: "Guru honorer",
|
||||
penghasilan: 2500000,
|
||||
kondisi_rumah: 6,
|
||||
kepemilikan_aset: 6,
|
||||
fuzzy_score: 15.3,
|
||||
fuzzy_label: "TIDAK PRIORITAS",
|
||||
},
|
||||
];
|
||||
|
||||
// ─── POI Markers (poi_markers) ───────────────────────────────────────────────
|
||||
const SEED_POI = [
|
||||
{ name: "Masjid Jami' Pontianak", poi_type: "religion", religion_subtype: "mosque", latitude: -0.0220, longitude: 109.3420, notes: "Masjid terbesar di pusat kota", radius_meters: 500 },
|
||||
{ name: "Gereja Katedral Pontianak", poi_type: "religion", religion_subtype: "church", latitude: -0.0245, longitude: 109.3380, notes: "Gereja bersejarah", radius_meters: 300 },
|
||||
{ name: "Vihara Bodhisattva Karaniya", poi_type: "religion", religion_subtype: "temple", latitude: -0.0198, longitude: 109.3455, notes: "Vihara terbesar", radius_meters: 200 },
|
||||
{ name: "Puskesmas Pontianak Selatan", poi_type: "clinic", religion_subtype: null, latitude: -0.0310, longitude: 109.3350, notes: "Puskesmas utama kecamatan selatan", radius_meters: 1000 },
|
||||
{ name: "Klinik Pratama Sehat Sentosa", poi_type: "clinic", religion_subtype: null, latitude: -0.0270, longitude: 109.3500, notes: "Klinik 24 jam", radius_meters: 800 },
|
||||
{ name: "Lumbung Pangan Desa Siantan", poi_type: "food-bank",religion_subtype: null, latitude: -0.0160, longitude: 109.3440, notes: "Distribusi beras untuk warga miskin",radius_meters: 1500 },
|
||||
{ name: "SDN 01 Pontianak Barat", poi_type: "school", religion_subtype: null, latitude: -0.0235, longitude: 109.3310, notes: "Sekolah dasar negeri", radius_meters: 600 },
|
||||
{ name: "SMP Negeri 3 Pontianak", poi_type: "school", religion_subtype: null, latitude: -0.0180, longitude: 109.3520, notes: "SMP favorit", radius_meters: 700 },
|
||||
];
|
||||
|
||||
// ─── Land Markers (land_markers) ─────────────────────────────────────────────
|
||||
const SEED_LAND_MARKERS = [
|
||||
{ name: "Tugu Khatulistiwa", marker_type: "marker", latitude: 0.0000, longitude: 109.3228, notes: "Monumen landmark utama" },
|
||||
{ name: "Kantor Gubernur Kalbar", marker_type: "flag", latitude: -0.0234, longitude: 109.3412, notes: "Pusat pemerintahan provinsi" },
|
||||
{ name: "Hutan Lindung Gunung Palung", marker_type: "protected", latitude: -1.2100, longitude: 110.1500, notes: "Kawasan hutan lindung nasional" },
|
||||
{ name: "BPN Pontianak", marker_type: "registry", latitude: -0.0280, longitude: 109.3360, notes: "Kantor pertanahan kota" },
|
||||
{ name: "Area Mangrove Kakap", marker_type: "protected", latitude: -0.1050, longitude: 109.1200, notes: "Zona konservasi mangrove" },
|
||||
{ name: "Terminal Ambawang", marker_type: "flag", latitude: -0.0800, longitude: 109.4100, notes: "Terminal bus utama" },
|
||||
];
|
||||
|
||||
// ─── Land Shapes (land_shapes) ───────────────────────────────────────────────
|
||||
const SEED_LAND_SHAPES = [
|
||||
{
|
||||
name: "Zona Permukiman Siantan",
|
||||
shape_type: "polygon",
|
||||
status: "Hak Milik",
|
||||
latitude: -0.0150,
|
||||
longitude: 109.3440,
|
||||
coordinates: JSON.stringify([
|
||||
[-0.0130, 109.3420], [-0.0130, 109.3460],
|
||||
[-0.0170, 109.3460], [-0.0170, 109.3420],
|
||||
]),
|
||||
radius_meters: null,
|
||||
notes: "Kawasan permukiman padat penduduk",
|
||||
},
|
||||
{
|
||||
name: "Lahan Pertanian Rasau Jaya",
|
||||
shape_type: "polygon",
|
||||
status: "Hak Guna Usaha",
|
||||
latitude: -0.1500,
|
||||
longitude: 109.3800,
|
||||
coordinates: JSON.stringify([
|
||||
[-0.1450, 109.3750], [-0.1450, 109.3850],
|
||||
[-0.1550, 109.3850], [-0.1550, 109.3750],
|
||||
]),
|
||||
radius_meters: null,
|
||||
notes: "Lahan pertanian padi, luas ~100 ha",
|
||||
},
|
||||
{
|
||||
name: "Area Industri Sei Raya",
|
||||
shape_type: "circle",
|
||||
status: "Hak Guna Bangunan",
|
||||
latitude: -0.0500,
|
||||
longitude: 109.3600,
|
||||
coordinates: null,
|
||||
radius_meters: 800,
|
||||
notes: "Kawasan industri ringan",
|
||||
},
|
||||
{
|
||||
name: "Jalan Tanjungpura",
|
||||
shape_type: "line",
|
||||
status: "Tanah Negara",
|
||||
latitude: -0.0250,
|
||||
longitude: 109.3400,
|
||||
coordinates: JSON.stringify([
|
||||
[-0.0200, 109.3350], [-0.0300, 109.3450],
|
||||
]),
|
||||
radius_meters: null,
|
||||
notes: "Jalan arteri utama kota, panjang ~3 km",
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Gas Station Markers (gas_station_markers) ───────────────────────────────
|
||||
const SEED_GAS_STATIONS = [
|
||||
{
|
||||
name: "SPBU 64.751.01 Pontianak",
|
||||
marker_category: "gas-pump",
|
||||
brand: "Pertamina",
|
||||
sub_type: null,
|
||||
gas_types: JSON.stringify(["Pertalite", "Pertamax", "Pertamina Dex", "Bio Solar"]),
|
||||
operating_hours: "24 Jam",
|
||||
status: null,
|
||||
latitude: -0.0230,
|
||||
longitude: 109.3390,
|
||||
notes: "SPBU utama di pusat kota",
|
||||
},
|
||||
{
|
||||
name: "SPBU 64.751.03 Ahmad Yani",
|
||||
marker_category: "gas-pump",
|
||||
brand: "Pertamina",
|
||||
sub_type: null,
|
||||
gas_types: JSON.stringify(["Pertalite", "Pertamax", "Bio Solar"]),
|
||||
operating_hours: "06:00 - 22:00",
|
||||
status: null,
|
||||
latitude: -0.0310,
|
||||
longitude: 109.3450,
|
||||
notes: "Dekat jalan Ahmad Yani",
|
||||
},
|
||||
{
|
||||
name: "SPBU 64.751.08 Siantan",
|
||||
marker_category: "gas-pump",
|
||||
brand: "Pertamina",
|
||||
sub_type: null,
|
||||
gas_types: JSON.stringify(["Pertalite", "Bio Solar"]),
|
||||
operating_hours: "06:00 - 21:00",
|
||||
status: null,
|
||||
latitude: -0.0155,
|
||||
longitude: 109.3520,
|
||||
notes: "SPBU kawasan Siantan",
|
||||
},
|
||||
{
|
||||
name: "Shell Gajah Mada",
|
||||
marker_category: "gas-pump",
|
||||
brand: "Shell",
|
||||
sub_type: null,
|
||||
gas_types: JSON.stringify(["Shell Super", "Shell V-Power", "Shell Diesel"]),
|
||||
operating_hours: "24 Jam",
|
||||
status: null,
|
||||
latitude: -0.0280,
|
||||
longitude: 109.3310,
|
||||
notes: "Shell pertama di Pontianak",
|
||||
},
|
||||
{
|
||||
name: "SPBU 64.751.12 Kubu Raya",
|
||||
marker_category: "gas-pump",
|
||||
brand: "Pertamina",
|
||||
sub_type: null,
|
||||
gas_types: JSON.stringify(["Pertalite", "Pertamax", "Bio Solar"]),
|
||||
operating_hours: "06:00 - 22:00",
|
||||
status: null,
|
||||
latitude: -0.0620,
|
||||
longitude: 109.3700,
|
||||
notes: "SPBU di kabupaten Kubu Raya",
|
||||
},
|
||||
{
|
||||
name: "EV Charging PLN Pontianak",
|
||||
marker_category: "charging-station",
|
||||
brand: null,
|
||||
sub_type: "Type 2 AC",
|
||||
gas_types: null,
|
||||
operating_hours: "24 Jam",
|
||||
status: "Active",
|
||||
latitude: -0.0240,
|
||||
longitude: 109.3430,
|
||||
notes: "Stasiun pengisian kendaraan listrik PLN",
|
||||
},
|
||||
{
|
||||
name: "EV Charging Mall Ayani Mega",
|
||||
marker_category: "charging-station",
|
||||
brand: null,
|
||||
sub_type: "CCS2 DC",
|
||||
gas_types: null,
|
||||
operating_hours: "10:00 - 22:00",
|
||||
status: "Active",
|
||||
latitude: -0.0335,
|
||||
longitude: 109.3470,
|
||||
notes: "Fast charging di basement mall",
|
||||
},
|
||||
{
|
||||
name: "Bengkel Pontianak Motor",
|
||||
marker_category: "wrench",
|
||||
brand: null,
|
||||
sub_type: null,
|
||||
gas_types: null,
|
||||
operating_hours: "08:00 - 17:00",
|
||||
status: null,
|
||||
latitude: -0.0260,
|
||||
longitude: 109.3480,
|
||||
notes: "Bengkel umum motor dan mobil",
|
||||
},
|
||||
{
|
||||
name: "Bengkel Jaya Abadi",
|
||||
marker_category: "wrench",
|
||||
brand: null,
|
||||
sub_type: null,
|
||||
gas_types: null,
|
||||
operating_hours: "08:00 - 18:00",
|
||||
status: null,
|
||||
latitude: -0.0190,
|
||||
longitude: 109.3560,
|
||||
notes: "Bengkel spesialis AC dan kelistrikan",
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
SEED_HOUSEHOLDS,
|
||||
SEED_POI,
|
||||
SEED_LAND_MARKERS,
|
||||
SEED_LAND_SHAPES,
|
||||
SEED_GAS_STATIONS,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
require("dotenv").config();
|
||||
const app = require("./app");
|
||||
const initDb = require("./db-init");
|
||||
|
||||
const PORT = process.env.PORT || 4000;
|
||||
|
||||
initDb()
|
||||
.then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Auth server running on port ${PORT}`);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to initialize database:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
const pool = require("./db");
|
||||
|
||||
async function findByUsernameOrEmail(identifier) {
|
||||
const { rows } = await pool.query(
|
||||
"SELECT * FROM users WHERE username = $1 OR email = $1 LIMIT 1",
|
||||
[identifier]
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
module.exports = { findByUsernameOrEmail };
|
||||
Reference in New Issue
Block a user