diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..506b175 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +RATE_LIMIT_TTL_SEC=60 +RATE_LIMIT_REQ_COUNT_PUBLIC=20 +RATE_LIMIT_REQ_COUNT_AUTH=100 +JWT_SECRET_KEY= \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 02e46c1..dcf8935 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,7 @@ module.exports = { node: true, jest: true, }, - ignorePatterns: ['.eslintrc.js', 'test/app.e2e-spec.ts'], + ignorePatterns: ['.eslintrc.js', 'test/app.e2e-spec.ts', 'client/**'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', diff --git a/.gitignore b/.gitignore index 22f55ad..ea48d65 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +.env \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..04caf46 --- /dev/null +++ b/client/index.html @@ -0,0 +1,57 @@ + + + + Lightspell ⚡️ + + + + + + + +
+
+ +

+ Your gateway to enhanced cross-chain experiences! +

+
+ +
+

Generate Your API Key

+

+ Unlock the full potential of XCM-API by generating your own API key. + With an API key, you can access the enhanced features and higher + request limits. Start building cross-chain applications seamlessly! +

+
+

Requests Limitations

+

+ Without an API key, you have a limit of 20 requests per minute. Once + you generate an API key, your limit increases to 100 requests per + minute, allowing you to build and scale your applications + effectively. +

+
+
+
+

Please complete the reCAPTCHA.

+ + +
+
+
+ + diff --git a/client/logo.png b/client/logo.png new file mode 100644 index 0000000..3f7de9e Binary files /dev/null and b/client/logo.png differ diff --git a/client/script.js b/client/script.js new file mode 100644 index 0000000..df46a11 --- /dev/null +++ b/client/script.js @@ -0,0 +1,46 @@ +function submitForm(event) { + event.preventDefault(); + + var response = grecaptcha.getResponse(); + var captchaMessage = document.getElementById('captcha-message'); + + if (response.length === 0) { + captchaMessage.style.display = 'block'; + } else { + fetch('/auth/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ recaptchaResponse: response }), + }) + .then((response) => { + if (!response.ok) { + if (response.status === 403) { + captchaMessage.textContent = + 'Captcha verification failed. Please try again.'; + } else if (response.status === 500) { + captchaMessage.textContent = + 'Error verifying captcha on the server. Please try again later.'; + } else { + captchaMessage.textContent = + 'An error occurred during API request. Please try again.'; + } + captchaMessage.style.display = 'block'; + throw new Error('API request failed'); + } + return response.json(); + }) + .then((data) => { + if (data.api_key) { + sessionStorage.setItem('api_key', data.api_key); + window.location.href = 'show-api-key.html'; + } else { + console.error('API key not received in the response.'); + } + }) + .catch((error) => { + console.error('Error during API request:', error); + }); + } +} diff --git a/client/show-api-key.html b/client/show-api-key.html new file mode 100644 index 0000000..c627532 --- /dev/null +++ b/client/show-api-key.html @@ -0,0 +1,81 @@ + + + + API Key + + + + +
+

Your Generated API Key

+

Copy and save this API key for future use:

+
+ +
+ + + diff --git a/client/style.css b/client/style.css new file mode 100644 index 0000000..34423c2 --- /dev/null +++ b/client/style.css @@ -0,0 +1,111 @@ +body { + font-family: 'Open Sans', sans-serif; + font-size: 16px; + line-height: 1.6; + margin: 0; + padding: 0; + color: #333; + background-color: #f4f4f4; +} + +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.header { + text-align: center; + margin-bottom: 20px; +} + +.section { + margin-bottom: 30px; +} + +.section-title { + font-size: 24px; + margin-bottom: 10px; +} + +.section-content { + font-size: 16px; +} + +.logo { + max-width: 250px; + margin: 0 auto; + display: block; +} + +.api-form { + background-color: #f9f9f9; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.api-form label { + display: block; + margin-bottom: 8px; + font-weight: bold; +} + +.api-form input[type='text'], +.api-form input[type='email'] { + width: 100%; + padding: 10px; + margin-bottom: 20px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.api-form button[type='submit'] { + background-color: #007bff; + color: #fff; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + margin-top: 10px; +} + +.header-container { + color: #333; + padding: 20px 0; + text-align: center; +} +.header-logo { + max-width: 200px; + display: block; + margin: 0 auto; +} +.header-slogan { + font-size: 18px; + margin-top: 10px; +} + +#captcha-message { + display: none; + color: red; + font-size: 14px; + margin: 0; + margin-top: 8px; + margin-bottom: 8px; +} + +@media only screen and (max-width: 500px) { + .g-recaptcha { + transform: scale(0.77); + transform-origin: 0 0; + } +} + +@media only screen and (max-width: 300px) { + .g-recaptcha { + transform: scale(0.66); + transform-origin: 0 0; + } +} diff --git a/package.json b/package.json index 9698662..8efb0bd 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,19 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.1.0", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.0.0", + "@nestjs/serve-static": "^4.0.0", + "@nestjs/throttler": "4.2.0", "@paraspell/sdk": "^2.0.5", "@polkadot/api": "^10.9.1", "@polkadot/api-base": "^10.9.1", "@polkadot/apps-config": "^0.124.1", "@polkadot/types": "^10.9.1", + "axios": "^1.4.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "reflect-metadata": "^0.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7412f9c..d1e2f37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,15 +4,27 @@ dependencies: '@nestjs/common': specifier: ^10.0.0 version: 10.0.0(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^3.0.0 + version: 3.0.0(@nestjs/common@10.0.0)(reflect-metadata@0.1.13) '@nestjs/core': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/jwt': + specifier: ^10.1.0 + version: 10.1.0(@nestjs/common@10.0.0) '@nestjs/mapped-types': specifier: '*' version: 0.0.1(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) '@nestjs/platform-express': specifier: ^10.0.0 version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/serve-static': + specifier: ^4.0.0 + version: 4.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/throttler': + specifier: 4.2.0 + version: 4.2.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.1.13) '@paraspell/sdk': specifier: ^2.0.5 version: 2.0.5(@polkadot/api-base@10.9.1)(@polkadot/api@10.9.1)(@polkadot/apps-config@0.124.1)(@polkadot/types@10.9.1) @@ -28,6 +40,9 @@ dependencies: '@polkadot/types': specifier: ^10.9.1 version: 10.9.1 + axios: + specifier: ^1.4.0 + version: 1.4.0 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -1338,6 +1353,20 @@ packages: tslib: 2.5.3 uid: 2.0.2 + /@nestjs/config@3.0.0(@nestjs/common@10.0.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-fzASk1Uv6AjdE6uA1na8zpqRCXAhRpcfgpCVv3SAKlgJ3VR3bEjcI4G17WHLgLBsmPzI1ofdkSI451WLD1F1Rw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + dotenv: 16.1.4 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + reflect-metadata: 0.1.13 + uuid: 9.0.0 + dev: false + /@nestjs/core@10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-HFTdj4vsF+2qOaq97ZPRDle6Q/KyL5lmMah0/ZR0ie+e1/tnlvmlqw589xFACTemLJFFOjZMy763v+icO9u72w==} requiresBuild: true @@ -1369,6 +1398,16 @@ packages: transitivePeerDependencies: - encoding + /@nestjs/jwt@10.1.0(@nestjs/common@10.0.0): + resolution: {integrity: sha512-iLwCGS25ybUxGS7i5j/Mwuyzvp/WxJftHlm8aLEBv5GV92apz6L1QVjxLdZrqXbzo++C8gdJauhzil8qitY+6w==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.2 + jsonwebtoken: 9.0.0 + dev: false + /@nestjs/mapped-types@0.0.1(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): resolution: {integrity: sha512-4G4Ui7Sj0UqXiZsUFk/6cPD3K7uZEFSElzkOftaJ3/lXW+HUi1/vfWXabF53qrzO1enTRQDxt1plDbP6SsqXEg==} peerDependencies: @@ -1412,6 +1451,27 @@ packages: - chokidar dev: true + /@nestjs/serve-static@4.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0): + resolution: {integrity: sha512-8cTrNV2ngdHIjiLNsXePnw0+KY1ThrZGz/WeyAG5gIvmZNDbnZBOrPoYlKL+MOzlXlQStxR5jKLYmn+nJeoncQ==} + peerDependencies: + '@fastify/static': ^6.5.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + express: ^4.18.1 + fastify: ^4.7.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + express: + optional: true + fastify: + optional: true + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + path-to-regexp: 0.2.5 + dev: false + /@nestjs/testing@10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-express@10.0.0): resolution: {integrity: sha512-U5q3+svkddpdSk51ZFCEnFpQuWxAwE4ahsX77FrqqCAYidr7HUtL/BHYOVzI5H9vUH6BvJxMbfo3tiUXQl/2aA==} peerDependencies: @@ -1431,6 +1491,19 @@ packages: tslib: 2.5.3 dev: true + /@nestjs/throttler@4.2.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-bn14AQRxJ5i5JKP37sQkdPr10Ld01xD0HqRmOC9s3HzVYyDpzyHShkg30+WUTFSPJCXTIRBovmgbSxr8LHG1Iw==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + md5: 2.3.0 + reflect-metadata: 0.1.13 + dev: false + /@noble/curves@1.1.0: resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} dependencies: @@ -3178,6 +3251,12 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/jsonwebtoken@9.0.2: + resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} + dependencies: + '@types/node': 20.3.1 + dev: false + /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: true @@ -3707,6 +3786,16 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + /axios@1.4.0: + resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /babel-jest@29.5.0(@babel/core@7.22.5): resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3907,6 +3996,10 @@ packages: node-int64: 0.4.0 dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3991,6 +4084,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -4224,6 +4321,10 @@ packages: which: 2.0.2 dev: true + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + /cuint@0.2.2: resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} dev: false @@ -4328,6 +4429,22 @@ packages: esutils: 2.0.3 dev: true + /dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dev: false + + /dotenv@16.1.4: + resolution: {integrity: sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==} + engines: {node: '>=12'} + dev: false + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ed2curve@0.3.0: resolution: {integrity: sha512-8w2fmmq3hv9rCrcI7g9hms2pMunQr1JINfcjwR9tAyZqhtyaMN991lF/ZfHfr5tzZQ8c7y7aBgZbjfbd0fjFwQ==} dependencies: @@ -4848,6 +4965,16 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /fork-ts-checker-webpack-plugin@8.0.0(typescript@5.1.3)(webpack@5.87.0): resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} engines: {node: '>=12.13.0', yarn: '>=1.0.0'} @@ -4887,7 +5014,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} @@ -5245,6 +5371,10 @@ packages: binary-extensions: 2.2.0 dev: true + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-core-module@2.12.1: resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} dependencies: @@ -5845,6 +5975,31 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonwebtoken@9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.5.3 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -5922,7 +6077,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /lru-cache@9.1.2: resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} @@ -5966,6 +6120,14 @@ packages: safe-buffer: 5.2.1 dev: false + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -6354,6 +6516,10 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + /path-to-regexp@0.2.5: + resolution: {integrity: sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==} + dev: false + /path-to-regexp@3.2.0: resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} @@ -6449,6 +6615,10 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -6695,7 +6865,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -7300,6 +7469,11 @@ packages: hasBin: true dev: false + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: false + /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true @@ -7552,7 +7726,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} diff --git a/src/app.module.ts b/src/app.module.ts index 49b8091..2adeb1e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,9 +4,49 @@ import { XTransferModule } from './x-transfer/x-transfer.module'; import { AssetsModule } from './assets/assets.module'; import { ChannelsModule } from './channels/channels.module'; import { PalletsModule } from './pallets/pallets.module'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthModule } from './auth/auth.module'; +import { AuthGuard } from './auth/auth.guard'; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { join } from 'path'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ - imports: [XTransferModule, AssetsModule, ChannelsModule, PalletsModule], + imports: [ + XTransferModule, + AssetsModule, + ChannelsModule, + PalletsModule, + AuthModule, + ConfigModule.forRoot({ isGlobal: true }), + ThrottlerModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + ttl: config.get('RATE_LIMIT_TTL_SEC'), + limit: (context) => { + const request = context.switchToHttp().getRequest(); + return request.user + ? config.get('RATE_LIMIT_REQ_COUNT_AUTH') + : config.get('RATE_LIMIT_REQ_COUNT_PUBLIC'); + }, + }), + }), + ServeStaticModule.forRoot({ + rootPath: join(__dirname, '..', 'client'), + renderPath: '/generate-api-key', + }), + ], controllers: [AppController], + providers: [ + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..910ef3a --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,13 @@ +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @HttpCode(HttpStatus.OK) + @Post('generate') + generateApiKey(@Body('recaptchaResponse') recaptcha: string) { + return this.authService.generateApiKey(recaptcha); + } +} diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts new file mode 100644 index 0000000..9240eff --- /dev/null +++ b/src/auth/auth.guard.ts @@ -0,0 +1,31 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const apiKey = request.headers['x-api-key']; + + if (!apiKey) { + return true; + } + + try { + const decoded = this.jwtService.verify(apiKey); + request.user = decoded; + return true; + } catch (error) { + throw new ForbiddenException( + `The provided API key is not valid. Please generate a new one. Alternatively, if you want to use the API with free rate limiting, remove the key from the headers.`, + ); + } + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..b2233a3 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; + +@Module({ + imports: [ + JwtModule.registerAsync({ + global: true, + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET_KEY'), + }), + }), + ], + controllers: [AuthController], + providers: [AuthService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..3c4b597 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,41 @@ +import { + ForbiddenException, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import axios from 'axios'; + +@Injectable() +export class AuthService { + constructor(private jwtService: JwtService) {} + + async generateApiKey(recaptcha: string) { + const recaptchaSecretKey = '6LfL0oYnAAAAABX3l2hJrzxhoQZJQGfZKuUcZyTt'; + + const data = { + secret: recaptchaSecretKey, + response: recaptcha, + }; + + const response = await axios + .post('https://www.google.com/recaptcha/api/siteverify', null, { + params: data, + }) + .catch((error) => { + throw new InternalServerErrorException( + 'Error verifying reCAPTCHA: ' + error, + ); + }); + + const verificationResult = response.data; + if (verificationResult.success) { + const payload = {}; + return { + api_key: await this.jwtService.signAsync(payload), + }; + } else { + throw new ForbiddenException('Recaptcha verification failed'); + } + } +}