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 ⚡️
+
+
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+
+
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:
+
+
Copy
+
+
+
+
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');
+ }
+ }
+}