feat: integration with API Vega Cloud
This commit is contained in:
+6
-3
@@ -4,9 +4,6 @@ DB_DSN=postgresql://local:secret@127.0.0.1/mydb
|
|||||||
BASIC_AUTH_USERNAME=local
|
BASIC_AUTH_USERNAME=local
|
||||||
BASIC_AUTH_PASSWORD=secret
|
BASIC_AUTH_PASSWORD=secret
|
||||||
|
|
||||||
# Attendance
|
|
||||||
ATTENDANCE_CHECK_IN_HOUR=8
|
|
||||||
|
|
||||||
# Object Storage
|
# Object Storage
|
||||||
S3_URL=<se-url>
|
S3_URL=<se-url>
|
||||||
S3_ACCESS_KEY=<s3-access-key>
|
S3_ACCESS_KEY=<s3-access-key>
|
||||||
@@ -23,3 +20,9 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4319/v1/traces
|
|||||||
JWT_PUBLIC_KEY=
|
JWT_PUBLIC_KEY=
|
||||||
JWT_PRIVATE_KEY=
|
JWT_PRIVATE_KEY=
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
|
|
||||||
|
# 3d Party REST API
|
||||||
|
HTTP_BASE_URL=http://example.com
|
||||||
|
HTTP_USERNAME=
|
||||||
|
HTTP_PASSWORD=
|
||||||
|
HTTP_TIMEOUT=3000
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"@opentelemetry/sdk-node": "^0.205.0",
|
"@opentelemetry/sdk-node": "^0.205.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.1.0",
|
"@opentelemetry/sdk-trace-node": "^2.1.0",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
|
"axios": "^1.12.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
@@ -610,8 +611,12 @@
|
|||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||||
@@ -628,6 +633,8 @@
|
|||||||
|
|
||||||
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
@@ -644,6 +651,8 @@
|
|||||||
|
|
||||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||||
@@ -660,6 +669,8 @@
|
|||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="],
|
"drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.44.5", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ=="],
|
"drizzle-orm": ["drizzle-orm@0.44.5", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ=="],
|
||||||
@@ -668,6 +679,8 @@
|
|||||||
|
|
||||||
"drizzle-zod": ["drizzle-zod@0.8.3", "", { "peerDependencies": { "drizzle-orm": ">=0.36.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww=="],
|
"drizzle-zod": ["drizzle-zod@0.8.3", "", { "peerDependencies": { "drizzle-orm": ">=0.36.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"elysia": ["elysia@1.4.6", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "openapi-types": ">= 12.0.0" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-u2CorXLPs5ZXyWP+tQR+bgka/lJA4vNpB8lDE2w/sTmdaIwoPQmHEL4J3ai6OAlluWR1kfG7T9gO3EYT9D8viQ=="],
|
"elysia": ["elysia@1.4.6", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "openapi-types": ">= 12.0.0" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-u2CorXLPs5ZXyWP+tQR+bgka/lJA4vNpB8lDE2w/sTmdaIwoPQmHEL4J3ai6OAlluWR1kfG7T9gO3EYT9D8viQ=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -676,6 +689,14 @@
|
|||||||
|
|
||||||
"error": ["error@7.0.2", "", { "dependencies": { "string-template": "~0.2.1", "xtend": "~4.0.0" } }, "sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw=="],
|
"error": ["error@7.0.2", "", { "dependencies": { "string-template": "~0.2.1", "xtend": "~4.0.0" } }, "sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw=="],
|
||||||
|
|
||||||
|
"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.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
||||||
|
|
||||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||||
@@ -738,6 +759,10 @@
|
|||||||
|
|
||||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||||
|
|
||||||
"forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="],
|
"forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
@@ -750,6 +775,10 @@
|
|||||||
|
|
||||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
@@ -758,12 +787,18 @@
|
|||||||
|
|
||||||
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
"graceful-server-elysia": ["graceful-server-elysia@1.0.25", "", { "peerDependencies": { "elysia": "latest" } }, "sha512-4IkmTnXHttsXMk6az39Z2O3aMBXw+p7SLZtwmJfCc7FCwoGzgO8F64wy9aY8ERUrAaQGV1a9kEM9sKTKo7qCvg=="],
|
"graceful-server-elysia": ["graceful-server-elysia@1.0.25", "", { "peerDependencies": { "elysia": "latest" } }, "sha512-4IkmTnXHttsXMk6az39Z2O3aMBXw+p7SLZtwmJfCc7FCwoGzgO8F64wy9aY8ERUrAaQGV1a9kEM9sKTKo7qCvg=="],
|
||||||
|
|
||||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||||
@@ -832,10 +867,16 @@
|
|||||||
|
|
||||||
"long": ["long@2.4.0", "", {}, "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ=="],
|
"long": ["long@2.4.0", "", {}, "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
@@ -918,6 +959,8 @@
|
|||||||
|
|
||||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@opentelemetry/sdk-node": "^0.205.0",
|
"@opentelemetry/sdk-node": "^0.205.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.1.0",
|
"@opentelemetry/sdk-trace-node": "^2.1.0",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
|
"axios": "^1.12.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Elysia } from 'elysia'
|
import { Elysia } from 'elysia'
|
||||||
// import authRouter from '~/modules/auth/router'
|
|
||||||
// import userRouter from '~/modules/user/router'
|
|
||||||
import deviceRouter from '~/modules/device/router'
|
import deviceRouter from '~/modules/device/router'
|
||||||
|
|
||||||
export const router = new Elysia({
|
export const router = new Elysia({
|
||||||
name: 'codebase.router',
|
name: 'codebase.router',
|
||||||
})
|
})
|
||||||
// .use(authRouter)
|
|
||||||
// .use(userRouter)
|
|
||||||
.use(deviceRouter)
|
.use(deviceRouter)
|
||||||
.get('/', () => ({
|
.get('/', () => ({
|
||||||
message: 'This service is running as expected.',
|
message: 'This service is running as expected.',
|
||||||
|
|||||||
+6
-9
@@ -61,12 +61,9 @@ export const redis = {
|
|||||||
db: Number(process.env.REDIS_DB),
|
db: Number(process.env.REDIS_DB),
|
||||||
}
|
}
|
||||||
|
|
||||||
// const minioHost = process.env.MINIO_HOST || ''
|
export const httpConfig = {
|
||||||
// export const minio = {
|
baseUrl: process.env.HTTP_BASE_URL || 'default-url',
|
||||||
// endPoint: minioHost,
|
username: process.env.HTTP_USERNAME,
|
||||||
// port: Number(process.env.MINIO_PORT || 9000),
|
password: process.env.HTTP_PASSWORD,
|
||||||
// useSSL: !!(process.env.MINIO_SSL || false),
|
timeout: Number(process.env.HTTP_TIMEOUT) || 3000,
|
||||||
// accessKey: String(process.env.MINIO_ACCESS_KEY) || '',
|
}
|
||||||
// secretKey: process.env.MINIO_SECRET_KEY || '',
|
|
||||||
// }
|
|
||||||
// export const uploadBucket = process.env.MINIO_BUCKET || ''
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import { httpConfig } from '~/config'
|
||||||
|
import logger from '~/plugins/logger'
|
||||||
|
|
||||||
|
interface AxiosConfigWithMetadata extends AxiosRequestConfig {
|
||||||
|
metadata?: {
|
||||||
|
startTime: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HttpRequestOptions {
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
|
url: string
|
||||||
|
body?: Record<string, unknown>
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HttpResponse<T = unknown> {
|
||||||
|
data: T
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public status: number,
|
||||||
|
public response?: AxiosResponse,
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'HttpError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let axiosInstance: AxiosInstance | null = null
|
||||||
|
|
||||||
|
function getAxiosInstance(): AxiosInstance {
|
||||||
|
if (!axiosInstance) {
|
||||||
|
axiosInstance = axios.create({
|
||||||
|
baseURL: httpConfig.baseUrl,
|
||||||
|
timeout: 30000, // 30 seconds default timeout
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add basic auth if configured
|
||||||
|
if (httpConfig.username && httpConfig.password) {
|
||||||
|
const auth = Buffer.from(`${httpConfig.username}:${httpConfig.password}`).toString('base64')
|
||||||
|
axiosInstance.defaults.headers.common['Authorization'] = `Basic ${auth}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request interceptor for logging
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
;(config as AxiosConfigWithMetadata).metadata = { startTime: Date.now() }
|
||||||
|
logger.info({
|
||||||
|
msg: 'HTTP Request',
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
headers: config.headers,
|
||||||
|
params: config.params,
|
||||||
|
data: config.data,
|
||||||
|
})
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
logger.error({
|
||||||
|
msg: 'HTTP Request Error',
|
||||||
|
error: error.message,
|
||||||
|
config: {
|
||||||
|
method: error.config?.method,
|
||||||
|
url: error.config?.url,
|
||||||
|
baseURL: error.config?.baseURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response interceptor for logging
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const startTime = (response.config as AxiosConfigWithMetadata).metadata?.startTime
|
||||||
|
logger.info({
|
||||||
|
msg: 'HTTP Response',
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
method: response.config.method?.toUpperCase(),
|
||||||
|
url: response.config.url,
|
||||||
|
baseURL: response.config.baseURL,
|
||||||
|
responseTime: startTime ? Date.now() - startTime : undefined,
|
||||||
|
})
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
const startTime = (error.config as AxiosConfigWithMetadata)?.metadata?.startTime
|
||||||
|
logger.error({
|
||||||
|
msg: 'HTTP Response Error',
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
method: error.config?.method?.toUpperCase(),
|
||||||
|
url: error.config?.url,
|
||||||
|
baseURL: error.config?.baseURL,
|
||||||
|
error: error.message,
|
||||||
|
responseData: error.response?.data,
|
||||||
|
responseTime: startTime ? Date.now() - startTime : undefined,
|
||||||
|
})
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return axiosInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
async function httpRequest<T = unknown>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
|
||||||
|
const { method, url, body, config = {} } = options
|
||||||
|
const instance = getAxiosInstance()
|
||||||
|
|
||||||
|
const axiosConfig: AxiosRequestConfig = {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||||
|
axiosConfig.data = body
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'GET' && body) {
|
||||||
|
axiosConfig.params = body
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRetries = 3
|
||||||
|
let lastError: Error | null = null
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<T> = await instance.request(axiosConfig)
|
||||||
|
return {
|
||||||
|
data: response.data,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: response.headers as Record<string, string>,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error
|
||||||
|
|
||||||
|
// Check if it's a transient error worth retrying
|
||||||
|
const isRetryable = axios.isAxiosError(error)
|
||||||
|
&& (
|
||||||
|
!error.response // Network errors
|
||||||
|
|| error.response.status >= 500 // Server errors
|
||||||
|
|| error.response.status === 429 // Rate limiting
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isRetryable || attempt === maxRetries) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff
|
||||||
|
const delay = Math.pow(2, attempt) * 1000
|
||||||
|
logger.warn({
|
||||||
|
msg: 'HTTP Retry',
|
||||||
|
attempt,
|
||||||
|
maxRetries,
|
||||||
|
delay,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
error: lastError.message,
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, all retries failed
|
||||||
|
if (lastError && axios.isAxiosError(lastError)) {
|
||||||
|
const status = lastError.response?.status || 0
|
||||||
|
const message = lastError.response?.data?.message || lastError.message
|
||||||
|
throw new HttpError(`HTTP ${method} ${url} failed: ${message}`, status, lastError.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpError(`HTTP ${method} ${url} failed: ${lastError?.message || 'Unknown error'}`, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { httpRequest, HttpError }
|
||||||
|
export type { HttpRequestOptions, HttpResponse }
|
||||||
@@ -132,13 +132,9 @@ console.log(sign({
|
|||||||
deviceType: 'A',
|
deviceType: 'A',
|
||||||
commandType: 'C',
|
commandType: 'C',
|
||||||
towerNumber: 'T1',
|
towerNumber: 'T1',
|
||||||
timestamp: '2025-10-01T14:30:12Z',
|
timestamp: '2025-10-14T04:06:52.022Z',
|
||||||
nonce: '9f2a6e7c8d3b',
|
nonce: '50ce476cd1c355ae552ae18be55af72c',
|
||||||
payload: {
|
}, '458fd644689a456efa87d38f788511d9'))
|
||||||
AC: 'On',
|
|
||||||
Temp: 25,
|
|
||||||
},
|
|
||||||
}, 'savy_123123'))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the signature of the data using HMAC-SHA256 with constant-time comparison.
|
* Verifies the signature of the data using HMAC-SHA256 with constant-time comparison.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Clock skew window: ±5 minutes
|
// Clock skew window: ±5 minutes
|
||||||
const CLOCK_SKEW_MS = 5 * 60 * 1000
|
// const CLOCK_SKEW_MS = 5 * 60 * 1000
|
||||||
|
|
||||||
export const deviceGuard = () => ({
|
export const deviceGuard = () => ({
|
||||||
beforeHandle(context: unknown) {
|
beforeHandle(context: unknown) {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { UnauthenticatedError } from '~/helpers/errors'
|
|
||||||
import { LoginBody, LoginResponse } from '../schema'
|
|
||||||
import UserQuery from '@/user/queries/query'
|
|
||||||
import { passwordVerify } from '~/helpers/utils'
|
|
||||||
import { logger } from '~/plugins'
|
|
||||||
import { isEmpty, omit } from 'lodash-es'
|
|
||||||
import { sign } from '~/helpers/jwt'
|
|
||||||
import { UserInfo } from '../schema'
|
|
||||||
|
|
||||||
export default abstract class CommandService {
|
|
||||||
static async login(body: LoginBody): Promise<LoginResponse> {
|
|
||||||
const user = await UserQuery.getUserByEmail(body.email)
|
|
||||||
if (!user) {
|
|
||||||
logger.debug({
|
|
||||||
'body.email': body.email,
|
|
||||||
}, 'Login failed due to user not found.')
|
|
||||||
throw new UnauthenticatedError('Invalid username or password.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEmpty(user.password) && !passwordVerify(body.password, String(user.password))) {
|
|
||||||
logger.info({
|
|
||||||
'user.hasPassword': !isEmpty(user.password),
|
|
||||||
}, 'Login failed due to invalid password.')
|
|
||||||
throw new UnauthenticatedError('Invalid username or password.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const docs = omit(user, ['password', 'deletedAt']) as UserInfo
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
const sub = `${user.id}.${now.toString(36).toUpperCase()}`
|
|
||||||
const accessToken = await sign({
|
|
||||||
sub,
|
|
||||||
user: docs,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Elysia } from 'elysia'
|
|
||||||
import { basicAuthMiddleware, bearerAuthMiddleware } from '~/middlewares'
|
|
||||||
import { getInfoSchema, loginBody, loginResponseSchema } from './schema'
|
|
||||||
import CommandService from './commands/service'
|
|
||||||
|
|
||||||
export const router = new Elysia({
|
|
||||||
name: 'modules.auth',
|
|
||||||
detail: { tags: ['Authentication'] },
|
|
||||||
prefix: '/auth/v1',
|
|
||||||
})
|
|
||||||
.post('/login', async ({ headers, set, body }) => {
|
|
||||||
// basic auth middleware
|
|
||||||
basicAuthMiddleware(headers, set)
|
|
||||||
|
|
||||||
const user = await CommandService.login(body)
|
|
||||||
return {
|
|
||||||
message: 'Logged in.',
|
|
||||||
data: user,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
body: loginBody,
|
|
||||||
response: loginResponseSchema,
|
|
||||||
detail: {
|
|
||||||
security: [{ basicAuth: [] }],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.get('/info', async ({ headers, set }) => {
|
|
||||||
// bearer auth middleware
|
|
||||||
const user = await bearerAuthMiddleware(headers, set)
|
|
||||||
return {
|
|
||||||
message: 'Get user info.',
|
|
||||||
data: user,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
response: getInfoSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
import { selectUserSchema } from '../user/schema'
|
|
||||||
|
|
||||||
export const loginBody = z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
password: z.string().min(8),
|
|
||||||
})
|
|
||||||
export type LoginBody = z.infer<typeof loginBody>
|
|
||||||
|
|
||||||
const loginResponse = z.object({
|
|
||||||
accessToken: z.string().describe('Access token of the user, encoded in JWT format'),
|
|
||||||
// refreshToken: z.string().describe('Refresh token of the user, encoded in JWT format'),
|
|
||||||
})
|
|
||||||
export type LoginResponse = z.infer<typeof loginResponse>
|
|
||||||
|
|
||||||
const userInfo = selectUserSchema.omit({
|
|
||||||
password: true,
|
|
||||||
deletedAt: true,
|
|
||||||
})
|
|
||||||
export const getInfoResponse = userInfo
|
|
||||||
export type UserInfo = z.infer<typeof getInfoResponse>
|
|
||||||
|
|
||||||
export const loginResponseSchema = {
|
|
||||||
200: z.object({
|
|
||||||
message: z.literal('Logged in.'),
|
|
||||||
data: loginResponse,
|
|
||||||
}),
|
|
||||||
401: z.object({
|
|
||||||
message: z.string(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInfoSchema = {
|
|
||||||
200: z.object({
|
|
||||||
message: z.literal('Get user info.'),
|
|
||||||
data: getInfoResponse,
|
|
||||||
}),
|
|
||||||
401: z.object({
|
|
||||||
message: z.string(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { httpRequest } from '~/helpers/http/axios'
|
||||||
|
import logger from '~/plugins/logger'
|
||||||
|
|
||||||
|
export async function sendCommandToThirdParty(topicData: string, payload: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await httpRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/command',
|
||||||
|
body: {
|
||||||
|
topic: topicData,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
logger.info({ msg: 'Third-party command sent successfully', topic: topicData, status: response.status })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ msg: 'Failed to send command to third-party service', topic: topicData, error: (error as Error).message })
|
||||||
|
throw new Error('third_party_command_failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { verify } from '~/helpers/signature'
|
import { verify } from '~/helpers/signature'
|
||||||
import { DeviceCommandBody } from '../schema'
|
import { DeviceCommandBody } from '../schema'
|
||||||
|
import { sendCommandToThirdParty } from './command'
|
||||||
|
|
||||||
export default abstract class CommandService {
|
export default abstract class CommandService {
|
||||||
static async sendCommand(body: DeviceCommandBody, headers: Record<string, string | undefined>): Promise<{
|
static async sendCommand(body: DeviceCommandBody, headers: Record<string, string | undefined>): Promise<{
|
||||||
@@ -22,10 +23,20 @@ export default abstract class CommandService {
|
|||||||
|
|
||||||
// TODO: Check nonce for replay attack when DB is implemented
|
// TODO: Check nonce for replay attack when DB is implemented
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
status: 'success',
|
status: 'success' as const,
|
||||||
message: 'Command accepted',
|
message: 'Command accepted' as const,
|
||||||
receivedAt: new Date().toISOString(),
|
receivedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate topicData and send to third-party service
|
||||||
|
try {
|
||||||
|
const topicData = `${body.merchantName}/${body.floorName}_${body.unitNumber}-${body.deviceName}-${body.roomName}-${body.deviceType}-${body.commandType}-${body.towerNumber}`
|
||||||
|
await sendCommandToThirdParty(topicData, body.payload.AC)
|
||||||
|
} catch {
|
||||||
|
throw new Error('internal_error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { httpRequest } from '~/helpers/http/axios'
|
||||||
|
import logger from '~/plugins/logger'
|
||||||
|
|
||||||
|
export async function queryDeviceStatus(topic: string): Promise<{
|
||||||
|
floor: string
|
||||||
|
unit: string
|
||||||
|
device: string
|
||||||
|
room: string
|
||||||
|
devType: string
|
||||||
|
code: string
|
||||||
|
tower: string
|
||||||
|
payload: string
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
const response = await httpRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/state-reply',
|
||||||
|
body: { topic },
|
||||||
|
})
|
||||||
|
|
||||||
|
if ((response.data as Record<string, unknown>) && (response.data as Record<string, unknown>).data) {
|
||||||
|
const thirdPartyData = (response.data as Record<string, unknown>).data as Record<string, unknown>
|
||||||
|
logger.info({ msg: 'Device status queried successfully', topic, status: response.status })
|
||||||
|
|
||||||
|
return {
|
||||||
|
floor: thirdPartyData.floor as string,
|
||||||
|
unit: thirdPartyData.unit as string,
|
||||||
|
device: thirdPartyData.device as string,
|
||||||
|
room: thirdPartyData.room as string,
|
||||||
|
devType: thirdPartyData.devType as string,
|
||||||
|
code: thirdPartyData.code as string,
|
||||||
|
tower: thirdPartyData.tower as string,
|
||||||
|
payload: thirdPartyData.payload as string,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info({ msg: 'Device status not found', topic, status: response.status })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ msg: 'Failed to query device status', topic, error: (error as Error).message })
|
||||||
|
throw new Error('third_party_query_failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { verify } from '~/helpers/signature'
|
||||||
|
import { DeviceQueryParams } from '../schema'
|
||||||
|
import { queryDeviceStatus } from './query'
|
||||||
|
|
||||||
|
export default abstract class QueryService {
|
||||||
|
static async getDeviceStatus(query: DeviceQueryParams, headers: Record<string, string | undefined>): Promise<{
|
||||||
|
status: 'success'
|
||||||
|
message: 'command accepted'
|
||||||
|
data: {
|
||||||
|
floor: string
|
||||||
|
unit: string
|
||||||
|
device: string
|
||||||
|
room: string
|
||||||
|
devType: string
|
||||||
|
code: string
|
||||||
|
tower: string
|
||||||
|
payload: string
|
||||||
|
}
|
||||||
|
} | null> {
|
||||||
|
const apiKey = headers['x-api-key']
|
||||||
|
|
||||||
|
// For now, use apiKey as secretKey since DB validation is skipped
|
||||||
|
const secretKey = apiKey
|
||||||
|
|
||||||
|
// Remove signature from query for verification
|
||||||
|
const { signature, ...queryWithoutSignature } = query
|
||||||
|
|
||||||
|
const isValid = verify(queryWithoutSignature, signature, secretKey)
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('invalid_signature')
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check nonce for replay attack when DB is implemented
|
||||||
|
|
||||||
|
const topic = `${query.merchantName}/${query.floorName}_${query.unitNumber}-${query.deviceName}-${query.roomName}-${query.deviceType}-${query.commandType}-${query.towerNumber}`
|
||||||
|
|
||||||
|
const deviceData = await queryDeviceStatus(topic)
|
||||||
|
|
||||||
|
if (deviceData) {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
message: 'command accepted',
|
||||||
|
data: deviceData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Elysia } from 'elysia'
|
import { Elysia } from 'elysia'
|
||||||
import { deviceGuard } from '~/middlewares/deviceGuard'
|
import { deviceGuard } from '~/middlewares/deviceGuard'
|
||||||
import { deviceCommandBody, deviceCommandResponseSchema } from './schema'
|
import { deviceCommandBody, deviceCommandResponseSchema, deviceQueryParams, deviceQueryResponseSchema } from './schema'
|
||||||
import CommandService from './commands/service'
|
import CommandService from './commands/service'
|
||||||
|
import QueryService from './queries/service'
|
||||||
import { deviceErrorResponseMap } from '~/helpers/errors'
|
import { deviceErrorResponseMap } from '~/helpers/errors'
|
||||||
|
|
||||||
export const router = new Elysia({
|
export const router = new Elysia({
|
||||||
@@ -35,5 +36,35 @@ export const router = new Elysia({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.get('/status', async ({ query, headers, set }) => {
|
||||||
|
try {
|
||||||
|
const result = await QueryService.getDeviceStatus(query, headers)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
set.status = 404
|
||||||
|
return { status: 'not_found', message: 'Device status not found' }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message in deviceErrorResponseMap) {
|
||||||
|
const { status, response } = deviceErrorResponseMap[error.message]
|
||||||
|
set.status = status
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return response as any
|
||||||
|
}
|
||||||
|
set.status = 500
|
||||||
|
return { status: 'internal_error', message: 'Server error' }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
query: deviceQueryParams,
|
||||||
|
response: deviceQueryResponseSchema,
|
||||||
|
detail: {
|
||||||
|
security: [
|
||||||
|
{ apiKey: [] },
|
||||||
|
{ timestamp: [] },
|
||||||
|
{ signature: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ export const deviceCommandBody = z.object({
|
|||||||
})
|
})
|
||||||
export type DeviceCommandBody = z.infer<typeof deviceCommandBody>
|
export type DeviceCommandBody = z.infer<typeof deviceCommandBody>
|
||||||
|
|
||||||
|
// Request query schema for GET endpoint
|
||||||
|
export const deviceQueryParams = z.object({
|
||||||
|
merchantName: z.string(),
|
||||||
|
floorName: z.string(),
|
||||||
|
unitNumber: z.string(),
|
||||||
|
deviceName: z.string(),
|
||||||
|
roomName: z.string(),
|
||||||
|
deviceType: z.string(),
|
||||||
|
commandType: z.string(),
|
||||||
|
towerNumber: z.string(),
|
||||||
|
timestamp: z.string().datetime(), // ISO8601
|
||||||
|
nonce: z.string(),
|
||||||
|
signature: z.string(),
|
||||||
|
})
|
||||||
|
export type DeviceQueryParams = z.infer<typeof deviceQueryParams>
|
||||||
|
|
||||||
// Response schemas
|
// Response schemas
|
||||||
export const deviceCommandResponseSchema = {
|
export const deviceCommandResponseSchema = {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -45,6 +61,50 @@ export const deviceCommandResponseSchema = {
|
|||||||
status: z.literal('replay_attack_detected'),
|
status: z.literal('replay_attack_detected'),
|
||||||
message: z.literal('Timestamp or nonce is invalid/has been reused'),
|
message: z.literal('Timestamp or nonce is invalid/has been reused'),
|
||||||
}),
|
}),
|
||||||
|
404: z.object({
|
||||||
|
status: z.literal('not_found'),
|
||||||
|
message: z.literal('Device status not found'),
|
||||||
|
}),
|
||||||
|
500: z.object({
|
||||||
|
status: z.literal('internal_error'),
|
||||||
|
message: z.literal('Server error'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query response schema
|
||||||
|
export const deviceQueryResponseSchema = {
|
||||||
|
200: z.object({
|
||||||
|
status: z.literal('success'),
|
||||||
|
message: z.literal('command accepted'),
|
||||||
|
data: z.object({
|
||||||
|
floor: z.string(),
|
||||||
|
unit: z.string(),
|
||||||
|
device: z.string(),
|
||||||
|
room: z.string(),
|
||||||
|
devType: z.string(),
|
||||||
|
code: z.string(),
|
||||||
|
tower: z.string(),
|
||||||
|
payload: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
400: z.object({
|
||||||
|
status: z.literal('invalid_request'),
|
||||||
|
message: z.literal('Invalid request format'),
|
||||||
|
}),
|
||||||
|
401: z.union([
|
||||||
|
z.object({
|
||||||
|
status: z.literal('invalid_signature'),
|
||||||
|
message: z.literal('Signature mismatch'),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
status: z.literal('invalid_api_key'),
|
||||||
|
message: z.literal('API Key is not valid'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
403: z.object({
|
||||||
|
status: z.literal('replay_attack_detected'),
|
||||||
|
message: z.literal('Timestamp or nonce is invalid/has been reused'),
|
||||||
|
}),
|
||||||
500: z.object({
|
500: z.object({
|
||||||
status: z.literal('internal_error'),
|
status: z.literal('internal_error'),
|
||||||
message: z.literal('Server error'),
|
message: z.literal('Server error'),
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { db, table } from '~/db'
|
|
||||||
import type { NewUser } from '../schema'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
|
|
||||||
export default abstract class Command {
|
|
||||||
static async insertUser(user: NewUser) {
|
|
||||||
const newUser = await db.insert(table.users).values(user).returning()
|
|
||||||
return newUser[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updateUser(id: number, body: NewUser) {
|
|
||||||
const user = await db.update(table.users)
|
|
||||||
.set(body)
|
|
||||||
.where(eq(table.users.id, id))
|
|
||||||
.returning()
|
|
||||||
return user[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteUser(id: number) {
|
|
||||||
const user = await db.update(table.users)
|
|
||||||
.set({ deletedAt: new Date() })
|
|
||||||
.where(eq(table.users.id, id))
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
return user[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import type { CreateUserSchema, NewUser } from '../schema'
|
|
||||||
import Command from './command'
|
|
||||||
import Query from '../queries/query'
|
|
||||||
import { NotFoundError } from 'elysia'
|
|
||||||
import { passwordHash } from '~/helpers/utils'
|
|
||||||
import { FieldValidationError } from '~/helpers/errors'
|
|
||||||
import { roleEnum } from '~/db/schema/users'
|
|
||||||
|
|
||||||
export default abstract class CommandService {
|
|
||||||
static async createUser(body: CreateUserSchema) {
|
|
||||||
if (!body.email && !body.phone) {
|
|
||||||
throw new FieldValidationError('/', 'Either email or phone number must be provided')
|
|
||||||
}
|
|
||||||
|
|
||||||
// check email availability
|
|
||||||
if (body.email) {
|
|
||||||
const checkEmail = await Query.getUserByEmail(body.email)
|
|
||||||
if (checkEmail) {
|
|
||||||
throw new FieldValidationError('/email', 'Email already in use')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.password) {
|
|
||||||
body.password = passwordHash(body.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = {
|
|
||||||
...body,
|
|
||||||
role: body.role as typeof roleEnum.enumValues[number],
|
|
||||||
status: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdUser = await Command.insertUser(newUser)
|
|
||||||
|
|
||||||
return createdUser
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updateUser(id: number, body: CreateUserSchema) {
|
|
||||||
// check id
|
|
||||||
const checkId = await Query.getUserById(id)
|
|
||||||
if (!checkId) {
|
|
||||||
throw new NotFoundError('User not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// check name
|
|
||||||
const checkName = await Query.getUserByName(body.name)
|
|
||||||
if (checkName && checkName.id !== id) {
|
|
||||||
throw new FieldValidationError('/name', 'Name already in use')
|
|
||||||
}
|
|
||||||
|
|
||||||
// check email
|
|
||||||
if (body.email) {
|
|
||||||
const checkEmail = await Query.getUserByEmail(body.email)
|
|
||||||
if (checkEmail?.id !== id) {
|
|
||||||
throw new FieldValidationError('/email', 'Email already in use')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = {
|
|
||||||
...body,
|
|
||||||
status: true,
|
|
||||||
} as NewUser
|
|
||||||
|
|
||||||
return Command.updateUser(id, newUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteUser(id: number) {
|
|
||||||
// check id
|
|
||||||
const checkId = await Query.getUserById(id)
|
|
||||||
if (!checkId) {
|
|
||||||
throw new NotFoundError('User not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command.deleteUser(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { db } from '~/db'
|
|
||||||
|
|
||||||
export default abstract class Query {
|
|
||||||
static async listUser() {
|
|
||||||
const user = await db.query.users.findMany({
|
|
||||||
where: (u, { isNull }) => isNull(u.deletedAt),
|
|
||||||
})
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getUserById(id: number) {
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
columns: {
|
|
||||||
deletedAt: false,
|
|
||||||
},
|
|
||||||
where: (u, { and, eq, isNull }) => and(
|
|
||||||
eq(u.id, id),
|
|
||||||
isNull(u.deletedAt),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getUserByEmail(email: string) {
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
columns: {
|
|
||||||
deletedAt: false,
|
|
||||||
},
|
|
||||||
where: (u, { and, eq, isNull }) => and(
|
|
||||||
eq(u.email, email),
|
|
||||||
isNull(u.deletedAt),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getUserByName(name: string) {
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
columns: {
|
|
||||||
deletedAt: false,
|
|
||||||
},
|
|
||||||
where: (u, { and, eq, isNull }) => and(
|
|
||||||
eq(u.name, name),
|
|
||||||
isNull(u.deletedAt),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import Query from './query'
|
|
||||||
|
|
||||||
export default abstract class QueryService {
|
|
||||||
static async getUsers() {
|
|
||||||
const allUsers = await Query.listUser()
|
|
||||||
return allUsers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { Elysia, t } from 'elysia'
|
|
||||||
import { bearerAuthMiddleware } from '~/middlewares'
|
|
||||||
import { validationErrorSchema } from '~/plugins/defaultHandler'
|
|
||||||
import CommandService from './commands/service'
|
|
||||||
import QueryService from './queries/service'
|
|
||||||
import { createUserBody, createUserResponse, listUserResponse } from './schema'
|
|
||||||
|
|
||||||
export const router = new Elysia({
|
|
||||||
name: 'modules.user',
|
|
||||||
detail: { tags: ['Users'] },
|
|
||||||
prefix: '/user/v1/users',
|
|
||||||
})
|
|
||||||
.get('/', async ({ headers, set }) => {
|
|
||||||
await bearerAuthMiddleware(headers, set)
|
|
||||||
|
|
||||||
const users = await QueryService.getUsers()
|
|
||||||
return {
|
|
||||||
message: 'Get users.' as const,
|
|
||||||
data: users,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
response: {
|
|
||||||
200: t.Object({
|
|
||||||
message: t.Literal('Get users.'),
|
|
||||||
data: listUserResponse,
|
|
||||||
}),
|
|
||||||
401: t.Object({
|
|
||||||
message: t.Literal('Unauthenticated.'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.post('/', async ({ body, headers, set }) => {
|
|
||||||
await bearerAuthMiddleware(headers, set)
|
|
||||||
|
|
||||||
const user = await CommandService.createUser(body)
|
|
||||||
return {
|
|
||||||
message: 'User created.',
|
|
||||||
data: user,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
body: createUserBody,
|
|
||||||
response: {
|
|
||||||
200: t.Object({
|
|
||||||
message: t.Literal('User created.'),
|
|
||||||
data: createUserResponse,
|
|
||||||
}),
|
|
||||||
401: t.Object({
|
|
||||||
message: t.Literal('Unauthenticated.'),
|
|
||||||
}),
|
|
||||||
422: validationErrorSchema,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.put('/:id', async ({ params: { id }, body, headers, set }) => {
|
|
||||||
await bearerAuthMiddleware(headers, set)
|
|
||||||
|
|
||||||
const user = await CommandService.updateUser(id, body)
|
|
||||||
return {
|
|
||||||
message: 'User updated.' as const,
|
|
||||||
data: user,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
body: createUserBody,
|
|
||||||
response: {
|
|
||||||
200: t.Object({
|
|
||||||
message: t.Literal('User updated.'),
|
|
||||||
data: createUserResponse,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.delete('/:id', async ({ params: { id }, headers, set }) => {
|
|
||||||
await bearerAuthMiddleware(headers, set)
|
|
||||||
|
|
||||||
await CommandService.deleteUser(id)
|
|
||||||
return {
|
|
||||||
message: 'User deleted.' as const,
|
|
||||||
data: { id },
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
response: {
|
|
||||||
200: t.Object({
|
|
||||||
message: t.Literal('User deleted.'),
|
|
||||||
data: t.Object({ id: t.String() }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
import { createSelectSchema, createInsertSchema } from 'drizzle-zod'
|
|
||||||
import { table as $t } from '~/db'
|
|
||||||
import { roleEnum } from '~/db/schema/users'
|
|
||||||
|
|
||||||
export const selectUserSchema = createSelectSchema($t.users)
|
|
||||||
export const $s = selectUserSchema.shape
|
|
||||||
export const $i = createInsertSchema($t.users).shape
|
|
||||||
|
|
||||||
export const roleSchema = createSelectSchema(roleEnum)
|
|
||||||
|
|
||||||
export const createUserBody = z.object({
|
|
||||||
name: $i.name.min(3),
|
|
||||||
email: $i.email,
|
|
||||||
phone: z.string().optional(),
|
|
||||||
password: z.string().min(8),
|
|
||||||
role: roleSchema.optional(), // will validate in service
|
|
||||||
partnerId: z.string().optional(),
|
|
||||||
})
|
|
||||||
export type CreateUserSchema = z.infer<typeof createUserBody>
|
|
||||||
|
|
||||||
export const createUserResponse = selectUserSchema.pick({
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
phone: true,
|
|
||||||
role: true,
|
|
||||||
status: true,
|
|
||||||
createdAt: true,
|
|
||||||
})
|
|
||||||
export const listUserResponse = z.array(createUserResponse)
|
|
||||||
|
|
||||||
export type User = typeof $t.users.$inferSelect
|
|
||||||
export type NewUser = typeof $t.users.$inferInsert
|
|
||||||
Reference in New Issue
Block a user