diff --git a/server/.env.example b/server/.env.example index 94379d9..ba1e2d0 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,11 +1,17 @@ +# Server +SERVER_PORT=3001 + +# Hono ERP API (museum sales data) +ERP_API_URL=https://hono-erp.azurewebsites.net +ERP_API_CODE=your-api-function-key +ERP_USERNAME=your-username +ERP_PASSWORD=your-password + # Salla OAuth Credentials (from Salla Partners dashboard) SALLA_CLIENT_ID=your_client_id_here SALLA_CLIENT_SECRET=your_client_secret_here SALLA_REDIRECT_URI=http://localhost:3001/auth/callback -# Server port -SALLA_SERVER_PORT=3001 - # After OAuth, these will be populated automatically # SALLA_ACCESS_TOKEN= # SALLA_REFRESH_TOKEN= diff --git a/server/package-lock.json b/server/package-lock.json index 3338fb0..700fd1d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,17 +1,593 @@ { - "name": "hihala-salla-server", + "name": "hihala-server", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hihala-salla-server", + "name": "hihala-server", "version": "1.0.0", "dependencies": { "axios": "^1.6.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "tsx": "^4.19.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/accepts": { @@ -300,6 +876,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -433,6 +1051,21 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -479,6 +1112,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -767,6 +1413,16 @@ "node": ">= 0.8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -934,6 +1590,26 @@ "node": ">=0.6" } }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -947,6 +1623,27 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/server/package.json b/server/package.json index e191a0f..a2f0673 100644 --- a/server/package.json +++ b/server/package.json @@ -1,16 +1,23 @@ { - "name": "hihala-salla-server", + "name": "hihala-server", "version": "1.0.0", - "description": "Backend server for Salla OAuth and API integration", - "main": "index.js", + "type": "module", + "description": "Backend server for ERP proxy and Salla integration", + "main": "src/index.ts", "scripts": { - "start": "node index.js", - "dev": "node --watch index.js" + "start": "tsx src/index.ts", + "dev": "tsx watch src/index.ts" }, "dependencies": { "axios": "^1.6.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "tsx": "^4.19.0", + "typescript": "^5.9.3" } } diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 0000000..e3fbaf0 --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,25 @@ +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: resolve(__dirname, '..', '.env') }); + +export const server = { + port: parseInt(process.env.SERVER_PORT || '3001', 10), +}; + +export const salla = { + clientId: process.env.SALLA_CLIENT_ID || '', + clientSecret: process.env.SALLA_CLIENT_SECRET || '', + redirectUri: process.env.SALLA_REDIRECT_URI || 'http://localhost:3001/auth/callback', + accessToken: process.env.SALLA_ACCESS_TOKEN || '', + refreshToken: process.env.SALLA_REFRESH_TOKEN || '', +}; + +export const erp = { + apiUrl: process.env.ERP_API_URL || '', + apiCode: process.env.ERP_API_CODE || '', + username: process.env.ERP_USERNAME || '', + password: process.env.ERP_PASSWORD || '', +}; diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..ffbe941 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,33 @@ +import express from 'express'; +import cors from 'cors'; +import { server, salla, erp } from './config'; +import sallaRoutes from './routes/salla'; +import erpRoutes from './routes/erp'; + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// Mount routes +app.use(sallaRoutes); +app.use('/api/erp', erpRoutes); + +app.listen(server.port, () => { + console.log(`\nServer running on http://localhost:${server.port}`); + + console.log('\nERP API:'); + if (erp.apiUrl && erp.username) { + console.log(` GET /api/erp/sales?startDate=...&endDate=...`); + console.log(` GET /api/erp/status`); + } else { + console.log(' WARNING: ERP_API_URL / ERP_USERNAME not set in .env'); + } + + console.log('\nSalla:'); + if (salla.clientId && salla.clientSecret) { + console.log(' GET /auth/login, /auth/callback, /auth/status'); + console.log(' GET /api/store, /api/orders, /api/products, /api/customers'); + } else { + console.log(' WARNING: SALLA_CLIENT_ID / SALLA_CLIENT_SECRET not set in .env'); + } +}); diff --git a/server/src/routes/erp.ts b/server/src/routes/erp.ts new file mode 100644 index 0000000..73897b4 --- /dev/null +++ b/server/src/routes/erp.ts @@ -0,0 +1,34 @@ +import { Router, Request, Response } from 'express'; +import { fetchSales, isConfigured } from '../services/erpClient'; + +const router = Router(); + +// GET /api/erp/sales?startDate=2025-01-01T00:00:00&endDate=2025-01-31T00:00:00 +router.get('/sales', async (req: Request, res: Response) => { + if (!isConfigured()) { + res.status(503).json({ error: 'ERP API not configured on server' }); + return; + } + + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + res.status(400).json({ error: 'startDate and endDate query params required' }); + return; + } + + try { + const data = await fetchSales(startDate as string, endDate as string); + res.json(data); + } catch (err) { + console.error('ERP fetch error:', (err as Error).message); + res.status(502).json({ error: 'Failed to fetch from ERP API', details: (err as Error).message }); + } +}); + +// GET /api/erp/status +router.get('/status', (_req: Request, res: Response) => { + res.json({ configured: isConfigured() }); +}); + +export default router; diff --git a/server/src/routes/salla.ts b/server/src/routes/salla.ts new file mode 100644 index 0000000..ca09a42 --- /dev/null +++ b/server/src/routes/salla.ts @@ -0,0 +1,160 @@ +import { Router, Request, Response } from 'express'; +import crypto from 'crypto'; +import axios from 'axios'; +import { salla } from '../config'; +import { getAuthStatus, setTokens, callSallaAPI } from '../services/sallaClient'; + +const router = Router(); +let oauthState: string | null = null; + +// OAuth: redirect to Salla authorization +router.get('/auth/login', (_req: Request, res: Response) => { + oauthState = crypto.randomBytes(16).toString('hex'); + + const authUrl = + `https://accounts.salla.sa/oauth2/auth?` + + `client_id=${salla.clientId}` + + `&redirect_uri=${encodeURIComponent(salla.redirectUri)}` + + `&response_type=code` + + `&scope=offline_access` + + `&state=${oauthState}`; + + res.redirect(authUrl); +}); + +// OAuth: handle callback +router.get('/auth/callback', async (req: Request, res: Response) => { + const { code, error, state } = req.query; + + if (error) { + res.status(400).json({ error: 'Authorization denied', details: error }); + return; + } + + if (!code) { + res.status(400).json({ error: 'No authorization code received' }); + return; + } + + if (oauthState && state && state !== oauthState) { + res.status(400).json({ error: 'Invalid state parameter' }); + return; + } + + try { + const response = await axios.post('https://accounts.salla.sa/oauth2/token', { + client_id: salla.clientId, + client_secret: salla.clientSecret, + grant_type: 'authorization_code', + code, + redirect_uri: salla.redirectUri, + }); + + setTokens(response.data.access_token, response.data.refresh_token); + + console.log('\n========================================'); + console.log('SALLA CONNECTED SUCCESSFULLY!'); + console.log('========================================'); + console.log('Add these to your .env file:'); + console.log(`SALLA_ACCESS_TOKEN=${response.data.access_token}`); + console.log(`SALLA_REFRESH_TOKEN=${response.data.refresh_token}`); + console.log('========================================\n'); + + res.send(` + + +

Salla Connected!

+

Authorization successful. You can close this window.

+ + + + `); + } catch (err: unknown) { + const axiosErr = err as { response?: { data: unknown }; message: string }; + console.error('Token exchange failed:', axiosErr.response?.data || axiosErr.message); + res.status(500).json({ error: 'Token exchange failed', details: axiosErr.response?.data }); + } +}); + +// Auth status +router.get('/auth/status', (_req: Request, res: Response) => { + res.json(getAuthStatus()); +}); + +// Salla API proxy endpoints +router.get('/api/store', async (_req: Request, res: Response) => { + try { + const data = await callSallaAPI('/store/info'); + res.json(data); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +router.get('/api/orders', async (req: Request, res: Response) => { + try { + const { page = 1, per_page = 50, status } = req.query; + let endpoint = `/orders?page=${page}&per_page=${per_page}`; + if (status) endpoint += `&status=${status}`; + const data = await callSallaAPI(endpoint); + res.json(data); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +router.get('/api/orders/:id', async (req: Request, res: Response) => { + try { + const data = await callSallaAPI(`/orders/${req.params.id}`); + res.json(data); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +router.get('/api/products', async (req: Request, res: Response) => { + try { + const { page = 1, per_page = 50 } = req.query; + const data = await callSallaAPI(`/products?page=${page}&per_page=${per_page}`); + res.json(data); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +router.get('/api/customers', async (req: Request, res: Response) => { + try { + const { page = 1, per_page = 50 } = req.query; + const data = await callSallaAPI(`/customers?page=${page}&per_page=${per_page}`); + res.json(data); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +router.get('/api/analytics/summary', async (_req: Request, res: Response) => { + try { + const [orders, products] = await Promise.all([ + callSallaAPI('/orders?per_page=100') as Promise<{ data?: Array<{ amounts?: { total?: { amount?: number; currency?: string } } }>; pagination?: { total?: number } }>, + callSallaAPI('/products?per_page=100') as Promise<{ data?: unknown[]; pagination?: { total?: number } }>, + ]); + + const ordersList = orders.data || []; + const totalRevenue = ordersList.reduce((sum: number, o) => sum + (o.amounts?.total?.amount || 0), 0); + const avgOrderValue = ordersList.length > 0 ? totalRevenue / ordersList.length : 0; + + res.json({ + orders: { total: orders.pagination?.total || ordersList.length, recent: ordersList.length }, + products: { total: products.pagination?.total || (products.data?.length || 0) }, + revenue: { + total: totalRevenue, + average_order: avgOrderValue, + currency: ordersList[0]?.amounts?.total?.currency || 'SAR', + }, + }); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +export default router; diff --git a/server/src/services/erpClient.ts b/server/src/services/erpClient.ts new file mode 100644 index 0000000..f7704b4 --- /dev/null +++ b/server/src/services/erpClient.ts @@ -0,0 +1,62 @@ +import { erp } from '../config'; + +let cachedToken: string | null = null; + +async function login(): Promise { + const res = await fetch(`${erp.apiUrl}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: erp.username, password: erp.password }), + }); + + if (!res.ok) { + throw new Error(`ERP login failed: ${res.status} ${res.statusText}`); + } + + const data = await res.json(); + cachedToken = data.token; + return cachedToken; +} + +async function getToken(): Promise { + if (cachedToken) return cachedToken; + return login(); +} + +export async function fetchSales(startDate: string, endDate: string): Promise { + const token = await getToken(); + + const url = new URL(`${erp.apiUrl}/api/getbydate`); + url.searchParams.set('startDate', startDate); + url.searchParams.set('endDate', endDate); + url.searchParams.set('code', erp.apiCode); + + const res = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + + // Token expired — re-login and retry once + if (res.status === 401) { + cachedToken = null; + const freshToken = await login(); + + const retry = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${freshToken}` }, + }); + + if (!retry.ok) { + throw new Error(`ERP fetch failed after re-login: ${retry.status}`); + } + return retry.json(); + } + + if (!res.ok) { + throw new Error(`ERP fetch failed: ${res.status} ${res.statusText}`); + } + + return res.json(); +} + +export function isConfigured(): boolean { + return !!(erp.apiUrl && erp.apiCode && erp.username && erp.password); +} diff --git a/server/src/services/sallaClient.ts b/server/src/services/sallaClient.ts new file mode 100644 index 0000000..29fc243 --- /dev/null +++ b/server/src/services/sallaClient.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { salla } from '../config'; + +let accessToken = salla.accessToken || null; +let refreshToken = salla.refreshToken || null; + +export function getAuthStatus() { + return { connected: !!accessToken, hasRefreshToken: !!refreshToken }; +} + +export function setTokens(access: string, refresh?: string) { + accessToken = access; + if (refresh) refreshToken = refresh; +} + +export async function refreshAccessToken(): Promise { + if (!refreshToken) throw new Error('No refresh token available'); + + const response = await axios.post('https://accounts.salla.sa/oauth2/token', { + client_id: salla.clientId, + client_secret: salla.clientSecret, + grant_type: 'refresh_token', + refresh_token: refreshToken, + }); + + accessToken = response.data.access_token; + if (response.data.refresh_token) { + refreshToken = response.data.refresh_token; + } + + return accessToken!; +} + +export async function callSallaAPI( + endpoint: string, + method: 'GET' | 'POST' = 'GET', + data: unknown = null +): Promise { + if (!accessToken) throw new Error('Not authenticated. Visit /auth/login first.'); + + try { + const response = await axios({ + method, + url: `https://api.salla.dev/admin/v2${endpoint}`, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + data, + }); + return response.data; + } catch (err: unknown) { + const axiosErr = err as { response?: { status: number } }; + if (axiosErr.response?.status === 401) { + await refreshAccessToken(); + return callSallaAPI(endpoint, method, data); + } + throw err; + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..6e25192 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts index 7e3c084..fa10e0b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,10 @@ export default defineConfig({ server: { port: 3000, proxy: { + '/api/erp': { + target: 'http://localhost:3001', + changeOrigin: true, + }, '/api': { target: 'http://localhost:8090', changeOrigin: true,