chore: init
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# librenav-server
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.2.10. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
178
bun.lock
Normal file
178
bun.lock
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "librenav-server",
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/google": "^1.2.19",
|
||||||
|
"@types/pg": "^8.15.1",
|
||||||
|
"ai": "^4.3.16",
|
||||||
|
"better-auth": "^1.2.7",
|
||||||
|
"hono": "^4.7.9",
|
||||||
|
"hono-rate-limiter": "^0.4.2",
|
||||||
|
"pg": "^8.16.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ai-sdk/google": ["@ai-sdk/google@1.2.19", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-Xgl6eftIRQ4srUdCzxM112JuewVMij5q4JLcNmHcB68Bxn9dpr3MVUSPlJwmameuiQuISIA8lMB+iRiRbFsaqA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||||
|
|
||||||
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
||||||
|
|
||||||
|
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.2.4", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
||||||
|
|
||||||
|
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
|
||||||
|
|
||||||
|
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||||
|
|
||||||
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.3.16", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.3.15", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg=="],
|
||||||
|
|
||||||
|
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.0", "", {}, "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="],
|
||||||
|
|
||||||
|
"@simplewebauthn/server": ["@simplewebauthn/server@13.1.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||||
|
|
||||||
|
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.15.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-YKHrkGWBX5+ivzvOQ66I0fdqsQTsvxqM0AGP2i0XrVZ9DP5VA/deEbTf7VuLPGpY7fJB9uGbkZ6KjVhuHcrTkQ=="],
|
||||||
|
|
||||||
|
"ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
|
||||||
|
|
||||||
|
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.2.7", "", { "dependencies": { "@better-auth/utils": "0.2.4", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.27.6", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-2hCB263GSrgetsMUZw8vv9O1e4S4AlYJW3P4e8bX9u3Q3idv4u9BzDFCblpTLuL4YjYovghMCN0vurAsctXOAQ=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.0.9", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
|
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.7.9", "", {}, "sha512-/EsCoR5h7N4yu01TDu9GMCCJa6ZLk5ZJIWFFGNawAXmd1Tp53+Wir4xm0D2X19bbykWUlzQG0+BvPAji6p9E8Q=="],
|
||||||
|
|
||||||
|
"hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="],
|
||||||
|
|
||||||
|
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||||
|
|
||||||
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
|
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
|
||||||
|
|
||||||
|
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.16.0", "", { "dependencies": { "pg-connection-string": "^2.9.0", "pg-pool": "^3.10.0", "pg-protocol": "^1.10.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.5" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.2.5", "", {}, "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.9.0", "", {}, "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.10.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.10.0", "", {}, "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
||||||
|
|
||||||
|
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
||||||
|
|
||||||
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
|
|
||||||
|
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
|
||||||
|
|
||||||
|
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
|
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||||
|
|
||||||
|
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
|
||||||
|
|
||||||
|
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||||
|
|
||||||
|
"pg/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=="],
|
||||||
|
|
||||||
|
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||||
|
|
||||||
|
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
}
|
||||||
|
}
|
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "librenav-server",
|
||||||
|
"module": "src/main.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/google": "^1.2.19",
|
||||||
|
"@types/pg": "^8.15.1",
|
||||||
|
"ai": "^4.3.16",
|
||||||
|
"better-auth": "^1.2.7",
|
||||||
|
"hono": "^4.7.9",
|
||||||
|
"hono-rate-limiter": "^0.4.2",
|
||||||
|
"pg": "^8.16.0"
|
||||||
|
}
|
||||||
|
}
|
194
src/ai.ts
Normal file
194
src/ai.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { Hono, type Context } from "hono";
|
||||||
|
import { streamText, tool } from "ai";
|
||||||
|
import { google } from "@ai-sdk/google";
|
||||||
|
import { stream } from "hono/streaming";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
export type OverpassResult = {
|
||||||
|
elements: OverpassElement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OverpassElement = {
|
||||||
|
type: "node" | "way" | "relation";
|
||||||
|
id: number;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
lat?: number; // Only for nodes
|
||||||
|
lon?: number; // Only for nodes
|
||||||
|
nodes?: number[]; // Only for ways
|
||||||
|
center?: {
|
||||||
|
lat: number; // Only for relations
|
||||||
|
lon: number; // Only for relations
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter";
|
||||||
|
|
||||||
|
export async function fetchPOI(
|
||||||
|
lat: number,
|
||||||
|
lon: number,
|
||||||
|
radius: number,
|
||||||
|
) {
|
||||||
|
return await fetch(OVERPASS_SERVER, {
|
||||||
|
method: "POST",
|
||||||
|
body: `[out:json];
|
||||||
|
(
|
||||||
|
node(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
|
||||||
|
way(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
|
||||||
|
relation(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
|
||||||
|
node(around:${radius}, ${lat}, ${lon})["shop"]["name"];
|
||||||
|
way(around:${radius}, ${lat}, ${lon})["shop"]["name"];
|
||||||
|
relation(around:${radius}, ${lat}, ${lon})["shop"]["name"];
|
||||||
|
node(around:${radius}, ${lat}, ${lon})["building"]["building"!="garage"];
|
||||||
|
way(around:${radius}, ${lat}, ${lon})["building"]["building"!="garage"];
|
||||||
|
node(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
|
||||||
|
way(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
|
||||||
|
);
|
||||||
|
out center tags;`
|
||||||
|
}).then(res => res.json() as Promise<OverpassResult>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDistance(aLat: number, aLon: number, lat: number, lon: number): number {
|
||||||
|
const R = 6371e3; // Earth radius in meters
|
||||||
|
const φ1 = lat * Math.PI / 180;
|
||||||
|
const φ2 = aLat * Math.PI / 180;
|
||||||
|
const Δφ = (aLat - lat) * Math.PI / 180;
|
||||||
|
const Δλ = (aLon - lon) * Math.PI / 180;
|
||||||
|
|
||||||
|
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByDistance(elements: OverpassElement[], lat: number, lng: number): OverpassElement[] {
|
||||||
|
return elements.sort((a: OverpassElement, b: OverpassElement) => {
|
||||||
|
const aLoc = a.center || a;
|
||||||
|
const bLoc = b.center || b;
|
||||||
|
return getDistance(aLoc.lat!, aLoc.lon!, lat, lng) - getDistance(bLoc.lat!, bLoc.lon!, lat, lng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post(c: Context) {
|
||||||
|
|
||||||
|
const body = await c.req.json();
|
||||||
|
const text = body.text ? body.text.trim() : "";
|
||||||
|
const coords = body.coords;
|
||||||
|
let tags: Record<string, string> | undefined = undefined;
|
||||||
|
if(coords && coords.lat && coords.lon) {
|
||||||
|
// fetch tags from OpenStreetMap using Overpass API
|
||||||
|
console.log("Fetching POI for coordinates:", coords.lat, coords.lon);
|
||||||
|
const res = await fetchPOI(coords.lat, coords.lon, 100);
|
||||||
|
const poi = sortByDistance(res.elements, coords.lat, coords.lon);
|
||||||
|
if(poi.length > 0) {
|
||||||
|
tags = poi[0]?.tags ?? {}; // Use the first element's tags
|
||||||
|
coords.lat = poi[0]?.lat ?? coords.lat; // Use the first element's lat if available
|
||||||
|
coords.lon = poi[0]?.lon ?? coords.lon; // Use the first element's lon if available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Received request with text:", text);
|
||||||
|
const prompt = JSON.stringify({
|
||||||
|
coords,
|
||||||
|
tags,
|
||||||
|
text
|
||||||
|
}, null, 2)
|
||||||
|
console.log("Generated prompt:", prompt);
|
||||||
|
const result = streamText({
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error in AI response:", error);
|
||||||
|
},
|
||||||
|
model: google("gemini-2.0-flash", {
|
||||||
|
// useSearchGrounding: true,
|
||||||
|
}), // key is in GOOGLE_GENERATIVE_AI_API_KEY env variable
|
||||||
|
system: `You are a guide for a user who is trying to find places to visit.
|
||||||
|
You may be given OSM tags of a place and your task is to describe the place in a way that is useful for the user.
|
||||||
|
If not, the user might provide you with a description of a place they are looking for. Fetch the tags of the place using overpass in that case.
|
||||||
|
Do not guess the tags of the place, always fetch them using Overpass API. Note that places might be a node, way or relation. You should handle all of them correctly (by not fetching for just nodes).
|
||||||
|
You might get questions at the end of the tags by the user for you to answer.
|
||||||
|
In that case, focus on the question only. Do not describe the place if you get a question.
|
||||||
|
If there is no question from the user, describe the place based on the tags provided.
|
||||||
|
Do not guess an answer if the tags do not provide enough information. Instead, fetch the website of the place to get more information.
|
||||||
|
If the user asks for something extremely unlikely this place would have, you can tell them that it is unlikely to have that feature.
|
||||||
|
Do not mention the tags to the user, just answer the question.
|
||||||
|
If the user asks in a language other than English, answer in that language.
|
||||||
|
Use the provided tools to query OpenStreetMap data if necessary/the user asks for information outside the provided place like asking what is around it.
|
||||||
|
For using the overpass tool, make sure to query for ways and relations as well, not just nodes.
|
||||||
|
DO NOT guess node, way or relation IDs when using the overpass tool, always use coordinates or names provided by the user.
|
||||||
|
IF THE USER DOES NOT PROVIDE COORDINATES, DO NOT GUESS THEM, INSTEAD USE THE PROVIDED TEXT TO QUERY OSM DATA. USE CITY NAMES FOR EXAMPLE.
|
||||||
|
Location of the place is given to help with querying OSM data. Note that there might be multiple ways to tag something you search for.
|
||||||
|
Example: amenity=kiosk and shop=kiosk are both valid ways to tag a kiosk.
|
||||||
|
DO NOT tell the user to use a mapping software or website, use the tools to query OSM data to answer the question.
|
||||||
|
If you would need to visit a website to answer the question, use the fetchWebsite tool to get the content of the website.
|
||||||
|
|
||||||
|
When describing a place, skip explaining the following tags as they are already displayed to the user: opening_hours, website, phone, email, any address tags
|
||||||
|
Focus on all other tags like wheelchair, amenity, healthcare:speciality, cuisine, etc.
|
||||||
|
|
||||||
|
When using tools, do not ask the user for confirmation, just use them directly.
|
||||||
|
|
||||||
|
The local date and time is ${new Date().toLocaleString("de-DE", { timeZone: "Europe/Berlin" })}. The users language is German.`,
|
||||||
|
prompt,
|
||||||
|
maxSteps: 5,
|
||||||
|
tools: {
|
||||||
|
overpass: tool({
|
||||||
|
description: "Query OpenStreetMap data using Overpass API with the given Overpass QL query.",
|
||||||
|
parameters: z.object({
|
||||||
|
query: z.string().describe("The Overpass QL query to execute."),
|
||||||
|
}),
|
||||||
|
execute: async ({ query }) => {
|
||||||
|
console.log("Executing Overpass API query:", query);
|
||||||
|
const response = await fetch("https://overpass-api.de/api/interpreter", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "text/plain" },
|
||||||
|
body: query,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Overpass API request failed: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.text();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
fetchWebsite: tool({
|
||||||
|
description: "Fetch the raw HTML content of a website.",
|
||||||
|
parameters: z.object({
|
||||||
|
url: z.string().describe("The full URL to fetch (from the OSM tags)"),
|
||||||
|
}),
|
||||||
|
execute: async ({ url }) => {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (compatible; GeminiBot/1.0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch site: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
function stripHTML(html: string): string {
|
||||||
|
// Remove script/style/head tags and their content
|
||||||
|
html = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
|
||||||
|
html = html.replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '');
|
||||||
|
html = html.replace(/<head[\s\S]*?>[\s\S]*?<\/head>/gi, '');
|
||||||
|
|
||||||
|
// Strip all remaining HTML tags
|
||||||
|
const text = html.replace(/<\/?[^>]+(>|$)/g, '');
|
||||||
|
|
||||||
|
return text.replace(/\s+/g, ' ').trim().slice(0, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stripHTML(text).slice(0, 5000); // avoid hitting token limit
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark the response as a v1 data stream:
|
||||||
|
c.header('X-Vercel-AI-Data-Stream', 'v1');
|
||||||
|
c.header('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
return stream(c, stream => stream.pipe(result.toDataStream()));
|
||||||
|
}
|
15
src/auth.ts
Normal file
15
src/auth.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { username } from "better-auth/plugins";
|
||||||
|
import { pool } from "./db";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: pool,
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
username({
|
||||||
|
minUsernameLength: 3
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
5
src/db.ts
Normal file
5
src/db.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL
|
||||||
|
})
|
135
src/main.ts
Normal file
135
src/main.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { auth } from "./auth";
|
||||||
|
import { cors } from "hono/cors";
|
||||||
|
import { pool } from "./db";
|
||||||
|
import { post } from "./ai";
|
||||||
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
|
|
||||||
|
const app = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
user: typeof auth.$Infer.Session.user | null;
|
||||||
|
session: typeof auth.$Infer.Session.session | null
|
||||||
|
}
|
||||||
|
}>();
|
||||||
|
|
||||||
|
async function setupDB() {
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS reviews (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
latitude FLOAT NOT NULL,
|
||||||
|
longitude FLOAT NOT NULL,
|
||||||
|
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||||
|
comment TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES "user"(id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setupDB();
|
||||||
|
|
||||||
|
app.use("*", async (c, next) => {
|
||||||
|
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
c.set("user", null);
|
||||||
|
c.set("session", null);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
c.set("user", session.user);
|
||||||
|
c.set("session", session.session);
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
"/api/*", // or replace with "*" to enable cors for all routes
|
||||||
|
cors({
|
||||||
|
origin: "*",
|
||||||
|
allowHeaders: ["Content-Type", "Authorization"],
|
||||||
|
allowMethods: ["POST", "GET", "OPTIONS"],
|
||||||
|
exposeHeaders: ["Content-Length"],
|
||||||
|
maxAge: 600,
|
||||||
|
credentials: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get("/api/config", (c) => {
|
||||||
|
const capabilities: string[] = ["auth", "reviews"];
|
||||||
|
|
||||||
|
if(process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
|
||||||
|
capabilities.push("ai");
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
name: "TrafficCue Server",
|
||||||
|
version: "0",
|
||||||
|
capabilities
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
||||||
|
|
||||||
|
app.get("/api/reviews", async (c) => {
|
||||||
|
let {lat, lon} = c.req.query();
|
||||||
|
if (!lat || !lon) {
|
||||||
|
return c.json({ error: "Latitude and longitude are required" }, 400);
|
||||||
|
}
|
||||||
|
// Remove unnecessary precision from lat/lon
|
||||||
|
lat = parseFloat(lat).toFixed(6);
|
||||||
|
lon = parseFloat(lon).toFixed(6);
|
||||||
|
console.log(`Fetching reviews for lat: ${lat}, lon: ${lon}`);
|
||||||
|
const res = await pool.query(
|
||||||
|
"SELECT * FROM reviews WHERE latitude = $1 AND longitude = $2",
|
||||||
|
[lat, lon],
|
||||||
|
);
|
||||||
|
return c.json(await Promise.all(res.rows.map(async (row) => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
user_id: row.user_id,
|
||||||
|
rating: row.rating,
|
||||||
|
comment: row.comment,
|
||||||
|
created_at: row.created_at,
|
||||||
|
username: await pool.query(
|
||||||
|
"SELECT username FROM \"user\" WHERE id = $1",
|
||||||
|
[row.user_id],
|
||||||
|
).then(res => res.rows[0]?.username || "Unknown"),
|
||||||
|
};
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/review", async (c) => {
|
||||||
|
const { rating, comment, lat, lon } = await c.req.json();
|
||||||
|
if (!rating || !lat || !lon) {
|
||||||
|
return c.json({ error: "Rating, latitude, and longitude are required" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = c.get("user");
|
||||||
|
if (!user) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await pool.query(
|
||||||
|
"INSERT INTO reviews (user_id, latitude, longitude, rating, comment) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||||
|
[user.id, lat, lon, rating, comment],
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(res.rows[0]);
|
||||||
|
})
|
||||||
|
|
||||||
|
if(process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
|
||||||
|
app.use("/api/ai", rateLimiter({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
limit: 50, // 10 requests per minute
|
||||||
|
standardHeaders: "draft-6",
|
||||||
|
keyGenerator: (c) => "global"
|
||||||
|
}))
|
||||||
|
app.post("/api/ai", post);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/", (c) => {
|
||||||
|
return c.text("TrafficCue Server");
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user