From d8538a83a26743e677df3e0e6de593c5845f2612 Mon Sep 17 00:00:00 2001 From: Volodymyr Derunov Date: Tue, 4 Mar 2025 18:12:02 +0200 Subject: [PATCH 1/5] Add api token --- server.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/server.js b/server.js index e71c302..e9bee23 100644 --- a/server.js +++ b/server.js @@ -134,6 +134,15 @@ const verifyMessage = async (req, res, next) => { }; const createJwtToken = async (walletAddress) => { + return new Promise((resolve, reject) => { + jwt.sign({ walletAddress }, process.env.JWT_SECRET_KEY, { expiresIn: '1d' }, (err, token) => { + if (err) reject(err); + resolve(token); + }); + }); +}; + +const createJwtApiToken = async (walletAddress) => { return new Promise((resolve, reject) => { jwt.sign({ walletAddress }, process.env.JWT_SECRET_KEY, {}, (err, token) => { if (err) reject(err); @@ -157,6 +166,21 @@ const saveTokenToGun = (walletAddress, encryptedToken) => { }); }; +const saveApiTokenToGun = (walletAddress, encryptedToken) => { + return new Promise((resolve, reject) => { + gun + .get(generateHash(walletAddress.toLowerCase())) + .get('api-tokens') + .put(encryptedToken, (ack) => { + if (ack.err) { + reject(new HttpError('Failed to save api token to Gun')); + } else { + resolve(); + } + }); + }); +}; + app.post('/api/verify', validateVerificationParameters, verifyMessage, async (_req, res, next) => { try { const token = await createJwtToken(res.locals.address); @@ -168,6 +192,22 @@ app.post('/api/verify', validateVerificationParameters, verifyMessage, async (_r } }); +app.post( + '/api/verify-api-token', + validateVerificationParameters, + verifyMessage, + async (_req, res, next) => { + try { + const apiToken = await createJwtApiToken(res.locals.address); + const encryptedApiToken = await encrypt(apiToken); + await saveApiTokenToGun(res.locals.address, encryptedApiToken); + res.status(200).send({ apiToken }); + } catch (err) { + next(err); + } + } +); + app.use( '/api/v0/cat', createProxyMiddleware({ @@ -226,6 +266,36 @@ const validateTokenWithGun = (walletAddress, token) => { }); }; +const validateApiTokenWithGun = (walletAddress, token) => { + return new Promise((resolve, reject) => { + gun + .get(generateHash(walletAddress.toLowerCase())) + .get('api-tokens') + .once(async (tokenData) => { + if (!tokenData || (await decrypt(tokenData)) !== token) { + reject(new HttpError('Forbidden', 403)); + } else { + resolve(); + } + }); + }); +}; + +const getApiTokenWithGun = (walletAddress) => { + return new Promise((resolve) => { + gun + .get(generateHash(walletAddress.toLowerCase())) + .get('api-tokens') + .once(async (tokenData) => { + if (!tokenData) { + resolve(null); + } else { + resolve(await decrypt(tokenData)); + } + }); + }); +}; + const authenticateToken = async (req, _res, next) => { try { const decoded = await verifyToken(req); @@ -311,6 +381,16 @@ app.get('/api/protected', authenticateToken, (_req, res) => { res.send('Hello! You are viewing protected content.'); }); +app.post('/api/generate-api-nonce', authenticateToken, createNonce); + +app.get('/api/api-token', authenticateToken, async (req, res, next) => { + try { + res.status(200).json({ apiToken: await getApiTokenWithGun(req.user.walletAddress) }); + } catch (err) { + next(err); + } +}); + const saveGeneratedKey = ({ walletAddress, key, id }) => { return new Promise((resolve, reject) => { gun @@ -530,14 +610,13 @@ app.get('/api/submitted-wallets', authenticateAdmin, async (_req, res, next) => } }); -app.post('/api/refresh-token', validateWalletAddress, authenticateToken, async (req, res, next) => { +app.post('/api/regenerate-api-token', authenticateToken, async (req, res, next) => { try { - const token = req.headers.authorization.split(' ')[1]; - await validateTokenWithGun(req.body.walletAddress, token); - const newToken = await createJwtToken(req.body.walletAddress); - const encryptedNewToken = await encrypt(generateHash(newToken)); - await saveTokenToGun(req.body.walletAddress, encryptedNewToken); - res.status(200).send({ token: newToken }); + await validateApiTokenWithGun(req.user.walletAddress, req.body.apiToken); + const newApiToken = await createJwtApiToken(req.user.walletAddress); + const encryptedNewApiToken = await encrypt(newApiToken); + await saveApiTokenToGun(req.user.walletAddress, encryptedNewApiToken); + res.status(200).send({ apiToken: newApiToken }); } catch (err) { next(err); } From feb561da06014fd6934bff3350cb7b90812ec081 Mon Sep 17 00:00:00 2001 From: Volodymyr Derunov Date: Wed, 5 Mar 2025 13:06:34 +0200 Subject: [PATCH 2/5] Save api token hash --- server.js | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/server.js b/server.js index e9bee23..b5ec88c 100644 --- a/server.js +++ b/server.js @@ -199,7 +199,7 @@ app.post( async (_req, res, next) => { try { const apiToken = await createJwtApiToken(res.locals.address); - const encryptedApiToken = await encrypt(apiToken); + const encryptedApiToken = await encrypt(generateHash(apiToken)); await saveApiTokenToGun(res.locals.address, encryptedApiToken); res.status(200).send({ apiToken }); } catch (err) { @@ -266,32 +266,13 @@ const validateTokenWithGun = (walletAddress, token) => { }); }; -const validateApiTokenWithGun = (walletAddress, token) => { - return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('api-tokens') - .once(async (tokenData) => { - if (!tokenData || (await decrypt(tokenData)) !== token) { - reject(new HttpError('Forbidden', 403)); - } else { - resolve(); - } - }); - }); -}; - -const getApiTokenWithGun = (walletAddress) => { +const checkApiTokenWithGun = (walletAddress) => { return new Promise((resolve) => { gun .get(generateHash(walletAddress.toLowerCase())) .get('api-tokens') - .once(async (tokenData) => { - if (!tokenData) { - resolve(null); - } else { - resolve(await decrypt(tokenData)); - } + .once((tokenData) => { + resolve(!!tokenData); }); }); }; @@ -383,9 +364,9 @@ app.get('/api/protected', authenticateToken, (_req, res) => { app.post('/api/generate-api-nonce', authenticateToken, createNonce); -app.get('/api/api-token', authenticateToken, async (req, res, next) => { +app.get('/api/check-api-token', authenticateToken, async (req, res, next) => { try { - res.status(200).json({ apiToken: await getApiTokenWithGun(req.user.walletAddress) }); + res.status(200).json({ apiTokenGenerated: await checkApiTokenWithGun(req.user.walletAddress) }); } catch (err) { next(err); } @@ -612,9 +593,8 @@ app.get('/api/submitted-wallets', authenticateAdmin, async (_req, res, next) => app.post('/api/regenerate-api-token', authenticateToken, async (req, res, next) => { try { - await validateApiTokenWithGun(req.user.walletAddress, req.body.apiToken); const newApiToken = await createJwtApiToken(req.user.walletAddress); - const encryptedNewApiToken = await encrypt(newApiToken); + const encryptedNewApiToken = await encrypt(generateHash(newApiToken)); await saveApiTokenToGun(req.user.walletAddress, encryptedNewApiToken); res.status(200).send({ apiToken: newApiToken }); } catch (err) { From 9416f2f0ed524f6a7bc79f7e840a5e77a41bac86 Mon Sep 17 00:00:00 2001 From: Volodymyr Derunov Date: Wed, 5 Mar 2025 14:51:37 +0200 Subject: [PATCH 3/5] Add req.query.api --- server.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index b5ec88c..545c0f7 100644 --- a/server.js +++ b/server.js @@ -266,6 +266,21 @@ const validateTokenWithGun = (walletAddress, token) => { }); }; +const validateApiTokenWithGun = (walletAddress, token) => { + return new Promise((resolve, reject) => { + gun + .get(generateHash(walletAddress.toLowerCase())) + .get('api-tokens') + .once(async (tokenData) => { + if (!tokenData || (await decrypt(tokenData)) !== generateHash(token)) { + reject(new HttpError('Unauthorized', 401)); + } else { + resolve(); + } + }); + }); +}; + const checkApiTokenWithGun = (walletAddress) => { return new Promise((resolve) => { gun @@ -285,7 +300,11 @@ const authenticateToken = async (req, _res, next) => { throw new HttpError('Unauthorized', 401); } const token = req.headers.authorization.split(' ')[1]; - await validateTokenWithGun(decoded.walletAddress, token); + if (req.query.api) { + await validateApiTokenWithGun(decoded.walletAddress, token); + } else { + await validateTokenWithGun(decoded.walletAddress, token); + } req.user = decoded; next(); } catch (err) { From dc1d5a839e14aee63c51f1d54e37593333c6681d Mon Sep 17 00:00:00 2001 From: Volodymyr Derunov Date: Fri, 14 Mar 2025 18:26:26 +0200 Subject: [PATCH 4/5] Moved pin add to a separate route --- server.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/server.js b/server.js index 545c0f7..625c775 100644 --- a/server.js +++ b/server.js @@ -884,21 +884,28 @@ app.use( pathRewrite: { '^/': '', }, + }) +); + +app.use( + '/api/v0/pin/add', + authenticateToken, + createProxyMiddleware({ + target: `${UPSTREAM_IPFS_CLUSTER_URL}/api/v0/pin/add`, + pathRewrite: { + '^/': '', + }, selfHandleResponse: true, on: { - proxyRes: responseInterceptor(async (responseBuffer, _proxyRes, req, res) => { + proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { try { res.removeHeader('trailer'); - if (_proxyRes.statusCode < 400) { - const cid = JSON.parse(responseBuffer.toString('utf8')).Root?.Cid['/']; - if (cid) { - await fetch(`${UPSTREAM_IPFS_CLUSTER_URL}/api/v0/pin/add?arg=${cid}`, { - method: 'POST', - }); + if (proxyRes.statusCode < 400) { + if (req.query.arg && req.query.customKey) { await addCidToGeneratedKey({ walletAddress: req.user.walletAddress, - key: req.query.key, - cid, + key: req.query.customKey, + cid: req.query.arg, }); } } From f7fcd2859e3731f52fedee2200e5b34a6c18fae3 Mon Sep 17 00:00:00 2001 From: Volodymyr Derunov Date: Sat, 15 Mar 2025 00:15:08 +0200 Subject: [PATCH 5/5] Refactor routes --- HttpError.js | 6 + contracts.js | 28 ++ createJwtApiToken.js | 12 + generateHash.js | 4 + gundb.js | 26 ++ removeMetaData.js | 4 + routeApiCheckApiToken.js | 19 ++ routeApiCids.js | 34 +++ routeApiRegenerateApiToken.js | 12 + routeApiRemoveCid.js | 44 +++ routeApiScreenshot.js | 38 +++ routeApiUniqueGeneratedKey.js | 20 ++ routeApiUniqueNamespace.js | 22 ++ routeApiV0KeyGen.js | 47 +++ routeApiV0KeyRm.js | 47 +++ routeApiV0NamePublish.js | 47 +++ routeApiV0PinAdd.js | 41 +++ routeApiVerify.js | 38 +++ routeApiVerifyApiToken.js | 12 + saveApiTokenToGun.js | 19 ++ server.js | 538 +++------------------------------- validateNamespace.js | 35 +++ 22 files changed, 592 insertions(+), 501 deletions(-) create mode 100644 HttpError.js create mode 100644 contracts.js create mode 100644 createJwtApiToken.js create mode 100644 generateHash.js create mode 100644 gundb.js create mode 100644 removeMetaData.js create mode 100644 routeApiCheckApiToken.js create mode 100644 routeApiCids.js create mode 100644 routeApiRegenerateApiToken.js create mode 100644 routeApiRemoveCid.js create mode 100644 routeApiScreenshot.js create mode 100644 routeApiUniqueGeneratedKey.js create mode 100644 routeApiUniqueNamespace.js create mode 100644 routeApiV0KeyGen.js create mode 100644 routeApiV0KeyRm.js create mode 100644 routeApiV0NamePublish.js create mode 100644 routeApiV0PinAdd.js create mode 100644 routeApiVerify.js create mode 100644 routeApiVerifyApiToken.js create mode 100644 saveApiTokenToGun.js create mode 100644 validateNamespace.js diff --git a/HttpError.js b/HttpError.js new file mode 100644 index 0000000..c60ffe7 --- /dev/null +++ b/HttpError.js @@ -0,0 +1,6 @@ +module.exports = class extends Error { + constructor(message, code) { + super(message); + this.code = code; + } +}; diff --git a/contracts.js b/contracts.js new file mode 100644 index 0000000..8a58c22 --- /dev/null +++ b/contracts.js @@ -0,0 +1,28 @@ +const { JsonRpcProvider, Contract } = require('ethers'); +const Whitelist = require('@synthetixio/synthetix-node-namespace/deployments/11155420/Whitelist'); +const Namespace = require('@synthetixio/synthetix-node-namespace/deployments/11155420/Namespace'); + +class EthereumContractError extends Error { + constructor(message, originalError) { + super(message); + this.name = 'EthereumContractError'; + this.originalError = originalError; + } +} + +const getContract = (address, abi) => { + try { + const provider = new JsonRpcProvider('https://sepolia.optimism.io'); + return new Contract(address, abi, provider); + } catch (err) { + throw new EthereumContractError('Failed to get contract', err); + } +}; + +const getNamespaceContract = () => getContract(Namespace.address, Namespace.abi); +const getWhitelistContract = () => getContract(Whitelist.address, Whitelist.abi); + +module.exports = { + getNamespaceContract, + getWhitelistContract, +}; diff --git a/createJwtApiToken.js b/createJwtApiToken.js new file mode 100644 index 0000000..dbbe7a1 --- /dev/null +++ b/createJwtApiToken.js @@ -0,0 +1,12 @@ +const jwt = require('jsonwebtoken'); + +const createJwtApiToken = async (walletAddress) => { + return new Promise((resolve, reject) => { + jwt.sign({ walletAddress }, process.env.JWT_SECRET_KEY, {}, (err, token) => { + if (err) reject(err); + resolve(token); + }); + }); +}; + +module.exports = createJwtApiToken; diff --git a/generateHash.js b/generateHash.js new file mode 100644 index 0000000..14c3539 --- /dev/null +++ b/generateHash.js @@ -0,0 +1,4 @@ +const crypto = require('node:crypto'); + +module.exports = (data) => + crypto.createHash('sha256').update(`${data}:${process.env.SECRET}`).digest('hex'); diff --git a/gundb.js b/gundb.js new file mode 100644 index 0000000..49bca87 --- /dev/null +++ b/gundb.js @@ -0,0 +1,26 @@ +const Gun = require('gun'); + +let gunInstance; + +const encrypt = async (data) => await Gun.SEA.encrypt(data, process.env.SECRET); +const decrypt = async (data) => await Gun.SEA.decrypt(data, process.env.SECRET); + +const initGun = (server) => { + if (!server) { + throw new Error('Server instance is required to initialize Gun'); + } + if (!gunInstance) { + gunInstance = Gun({ web: server, file: process.env.GUNDB_STORAGE_PATH }); + console.log('GunDB successfully initialized!'); + } + return gunInstance; +}; + +const getGun = () => { + if (!gunInstance) { + throw new Error('Gun is not initialized. Call initGun(server) first.'); + } + return gunInstance; +}; + +module.exports = { initGun, getGun, encrypt, decrypt }; diff --git a/removeMetaData.js b/removeMetaData.js new file mode 100644 index 0000000..09651f5 --- /dev/null +++ b/removeMetaData.js @@ -0,0 +1,4 @@ +module.exports = (o) => { + const { _, ...withoutMeta } = o; + return withoutMeta; +}; diff --git a/routeApiCheckApiToken.js b/routeApiCheckApiToken.js new file mode 100644 index 0000000..dec0c28 --- /dev/null +++ b/routeApiCheckApiToken.js @@ -0,0 +1,19 @@ +const checkApiTokenWithGun = (walletAddress) => { + return new Promise((resolve) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('api-tokens') + .once((tokenData) => { + resolve(!!tokenData); + }); + }); +}; + +module.exports = async (req, res, next) => { + try { + res.status(200).json({ apiTokenGenerated: await checkApiTokenWithGun(req.user.walletAddress) }); + } catch (err) { + next(err); + } +}; diff --git a/routeApiCids.js b/routeApiCids.js new file mode 100644 index 0000000..0471ef1 --- /dev/null +++ b/routeApiCids.js @@ -0,0 +1,34 @@ +const getCidsFromGeneratedKey = ({ walletAddress, key }) => { + return new Promise((resolve) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key) + .get('cids') + .once((node) => { + if (!node) { + return resolve([]); + } + + const cids = Object.entries(require('removeMetaData')(node)) + .filter(([_, value]) => value !== null) + .map(([key]) => key); + + resolve(cids); + }); + }); +}; + +module.exports = async (req, res, next) => { + try { + res.status(200).json({ + cids: await getCidsFromGeneratedKey({ + walletAddress: req.user.walletAddress, + key: req.query.key, + }), + }); + } catch (err) { + next(err); + } +}; diff --git a/routeApiRegenerateApiToken.js b/routeApiRegenerateApiToken.js new file mode 100644 index 0000000..51c5b91 --- /dev/null +++ b/routeApiRegenerateApiToken.js @@ -0,0 +1,12 @@ +const { encrypt } = require('./gundb'); + +module.exports = async (req, res, next) => { + try { + const newApiToken = await require('./createJwtApiToken')(req.user.walletAddress); + const encryptedNewApiToken = await encrypt(require('./generateHash')(newApiToken)); + await require('./saveApiTokenToGun')(req.user.walletAddress, encryptedNewApiToken); + res.status(200).send({ apiToken: newApiToken }); + } catch (err) { + next(err); + } +}; diff --git a/routeApiRemoveCid.js b/routeApiRemoveCid.js new file mode 100644 index 0000000..6064728 --- /dev/null +++ b/routeApiRemoveCid.js @@ -0,0 +1,44 @@ +const HttpError = require('./HttpError'); + +const deleteCidFromGeneratedKey = ({ walletAddress, key, cid }) => { + return new Promise((resolve) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key) + .get('cids') + .get(cid) + .put(null, resolve); + }); +}; + +module.exports = async (req, res, next) => { + try { + const { cid, key } = req.body; + + if (!cid) { + return next(new HttpError('CID missed.', 400)); + } + + const response = await fetch( + `${require('./env').UPSTREAM_IPFS_CLUSTER_URL}/api/v0/pin/rm?arg=${cid}`, + { + method: 'POST', + } + ); + if (!response.ok) { + throw new HttpError(`Failed to remove CID ${cid} from IPFS`); + } + + await deleteCidFromGeneratedKey({ + walletAddress: req.user.walletAddress, + key, + cid, + }); + + res.status(200).json({ success: true, cid }); + } catch (err) { + next(err); + } +}; diff --git a/routeApiScreenshot.js b/routeApiScreenshot.js new file mode 100644 index 0000000..31dcb8a --- /dev/null +++ b/routeApiScreenshot.js @@ -0,0 +1,38 @@ +const HttpError = require('./HttpError'); +const puppeteer = require('puppeteer'); + +module.exports = async (req, res, next) => { + const { url } = req.query; + + if (!url) { + return next(new HttpError('URL parameter is required', 400)); + } + + const urlPattern = /^(https?:\/\/[a-zA-Z0-9.-]+(:\d+)?\/ipns\/[a-zA-Z0-9\/_-]+)$/; + if (!urlPattern.test(url)) { + return next(new HttpError('Invalid url', 400)); + } + + let browser; + try { + browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + }); + const page = await browser.newPage(); + await page.setViewport({ width: 800, height: 600 }); + await page.goto(url, { waitUntil: 'networkidle2', timeout: 120000 }); + const screenshotBase64 = await page.screenshot({ + encoding: 'base64', + }); + await browser.close(); + + res.json({ image: `data:image/png;base64,${screenshotBase64}` }); + } catch { + next(new HttpError('Failed to generate screenshot', 500)); + } finally { + if (browser) { + await browser.close(); + } + } +}; diff --git a/routeApiUniqueGeneratedKey.js b/routeApiUniqueGeneratedKey.js new file mode 100644 index 0000000..ee8de77 --- /dev/null +++ b/routeApiUniqueGeneratedKey.js @@ -0,0 +1,20 @@ +const checkGeneratedKey = async ({ walletAddress, key }) => { + return require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key); +}; + +module.exports = async (req, res, next) => { + try { + const unique = await checkGeneratedKey({ + walletAddress: req.user.walletAddress, + key: req.body.key, + }); + + res.status(200).json({ unique: !unique }); + } catch (err) { + next(err); + } +}; diff --git a/routeApiUniqueNamespace.js b/routeApiUniqueNamespace.js new file mode 100644 index 0000000..190266e --- /dev/null +++ b/routeApiUniqueNamespace.js @@ -0,0 +1,22 @@ +module.exports = async (req, res, next) => { + try { + const NamespaceContract = require('./contracts').getNamespaceContract(); + + const tokenId = await NamespaceContract.namespaceToTokenId(req.body.namespace); + if (!tokenId) { + res.status(200).json({ unique: true }); + return; + } + const owner = await NamespaceContract.ownerOf(tokenId); + if (owner.toLowerCase() === req.user.walletAddress.toLowerCase()) { + res.status(200).json({ unique: true }); + return; + } + if (owner.toLowerCase() !== req.user.walletAddress.toLowerCase()) { + res.status(200).json({ unique: false }); + return; + } + } catch (err) { + next(err); + } +}; diff --git a/routeApiV0KeyGen.js b/routeApiV0KeyGen.js new file mode 100644 index 0000000..b037703 --- /dev/null +++ b/routeApiV0KeyGen.js @@ -0,0 +1,47 @@ +const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); +const HttpError = require('./HttpError'); + +const saveGeneratedKey = ({ walletAddress, key, id }) => { + return new Promise((resolve, reject) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key) + .put({ key, id, published: false }, (ack) => { + if (ack.err) { + reject(new HttpError(`Failed to save ipns keys to Gun: ${ack.err}`)); + } else { + resolve(); + } + }); + }); +}; + +module.exports = createProxyMiddleware({ + target: `${require('./env').UPSTREAM_IPFS_URL}/api/v0/key/gen`, + pathRewrite: { + '^/': '', + }, + selfHandleResponse: true, + on: { + proxyReq: function onProxyReq(proxyReq) { + proxyReq.removeHeader('authorization'); + }, + proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { + try { + res.removeHeader('trailer'); + if (proxyRes.statusCode < 400) { + await saveGeneratedKey({ + walletAddress: req.user.walletAddress, + key: req.query.arg, + id: JSON.parse(responseBuffer.toString('utf8')).Id, + }); + } + } catch (err) { + console.error('Error saving to Gun:', err.message); + } + return responseBuffer; + }), + }, +}); diff --git a/routeApiV0KeyRm.js b/routeApiV0KeyRm.js new file mode 100644 index 0000000..10dc647 --- /dev/null +++ b/routeApiV0KeyRm.js @@ -0,0 +1,47 @@ +const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); +const HttpError = require('./HttpError'); + +const deleteGeneratedKey = ({ walletAddress, key }) => { + return new Promise((resolve, reject) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key) + .put(null, (ack) => { + if (ack.err) { + reject(new HttpError(`Failed to delete key: ${ack.err}`)); + } else { + resolve(); + } + }); + }); +}; + +module.exports = createProxyMiddleware({ + target: `${require('./env').UPSTREAM_IPFS_URL}/api/v0/key/rm`, + pathRewrite: { + '^/': '', + }, + selfHandleResponse: true, + on: { + proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { + try { + res.removeHeader('trailer'); + if (proxyRes.statusCode < 400) { + await Promise.all( + JSON.parse(responseBuffer.toString('utf8')).Keys.map((k) => + deleteGeneratedKey({ + walletAddress: req.user.walletAddress, + key: k.Name, + }) + ) + ); + } + } catch (err) { + console.error('Error processing proxy response:', err); + } + return responseBuffer; + }), + }, +}); diff --git a/routeApiV0NamePublish.js b/routeApiV0NamePublish.js new file mode 100644 index 0000000..a91780f --- /dev/null +++ b/routeApiV0NamePublish.js @@ -0,0 +1,47 @@ +const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); +const HttpError = require('./HttpError'); + +const updateGeneratedKey = ({ walletAddress, key, updates }) => { + return new Promise((resolve, reject) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key) + .put(updates, (ack) => { + if (ack.err) { + reject(new HttpError(`Failed to update key: ${ack.err}`)); + } else { + resolve(); + } + }); + }); +}; + +module.exports = createProxyMiddleware({ + target: `${require('./env').UPSTREAM_IPFS_URL}/api/v0/name/publish`, + pathRewrite: { + '^/': '', + }, + selfHandleResponse: true, + on: { + proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { + try { + res.removeHeader('trailer'); + if (proxyRes.statusCode < 400) { + await updateGeneratedKey({ + walletAddress: req.user.walletAddress, + key: req.query.key, + updates: { + ipfs: JSON.parse(responseBuffer.toString('utf8')).Value, + published: true, + }, + }); + } + } catch (err) { + console.error('Error saving to Gun:', err.message); + } + return responseBuffer; + }), + }, +}); diff --git a/routeApiV0PinAdd.js b/routeApiV0PinAdd.js new file mode 100644 index 0000000..5c16cd9 --- /dev/null +++ b/routeApiV0PinAdd.js @@ -0,0 +1,41 @@ +const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); + +const addCidToGeneratedKey = ({ walletAddress, key, cid }) => { + return new Promise((resolve) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys') + .get(key) + .get('cids') + .get(cid) + .put({ cid }, resolve); + }); +}; + +module.exports = createProxyMiddleware({ + target: `${require('./env').UPSTREAM_IPFS_CLUSTER_URL}/api/v0/pin/add`, + pathRewrite: { + '^/': '', + }, + selfHandleResponse: true, + on: { + proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { + try { + res.removeHeader('trailer'); + if (proxyRes.statusCode < 400) { + if (req.query.arg && req.query.customKey) { + await addCidToGeneratedKey({ + walletAddress: req.user.walletAddress, + key: req.query.customKey, + cid: req.query.arg, + }); + } + } + } catch (e) { + console.error(e); + } + return responseBuffer; + }), + }, +}); diff --git a/routeApiVerify.js b/routeApiVerify.js new file mode 100644 index 0000000..2103e23 --- /dev/null +++ b/routeApiVerify.js @@ -0,0 +1,38 @@ +const jwt = require('jsonwebtoken'); +const { getGun, encrypt } = require('./gundb'); +const HttpError = require('./HttpError'); + +const createJwtToken = async (walletAddress) => { + return new Promise((resolve, reject) => { + jwt.sign({ walletAddress }, process.env.JWT_SECRET_KEY, { expiresIn: '1d' }, (err, token) => { + if (err) reject(err); + resolve(token); + }); + }); +}; + +const saveTokenToGun = (walletAddress, encryptedToken) => { + return new Promise((resolve, reject) => { + getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('tokens') + .put(encryptedToken, (ack) => { + if (ack.err) { + reject(new HttpError('Failed to save token to Gun')); + } else { + resolve(); + } + }); + }); +}; + +module.exports = async (_req, res, next) => { + try { + const token = await createJwtToken(res.locals.address); + const encryptedToken = await encrypt(require('./generateHash')(token)); + await saveTokenToGun(res.locals.address, encryptedToken); + res.status(200).send({ token }); + } catch (err) { + next(err); + } +}; diff --git a/routeApiVerifyApiToken.js b/routeApiVerifyApiToken.js new file mode 100644 index 0000000..31289bd --- /dev/null +++ b/routeApiVerifyApiToken.js @@ -0,0 +1,12 @@ +const { encrypt } = require('./gundb'); + +module.exports = async (_req, res, next) => { + try { + const apiToken = await require('./createJwtApiToken')(res.locals.address); + const encryptedApiToken = await encrypt(require('./generateHash')(apiToken)); + await require('./saveApiTokenToGun')(res.locals.address, encryptedApiToken); + res.status(200).send({ apiToken }); + } catch (err) { + next(err); + } +}; diff --git a/saveApiTokenToGun.js b/saveApiTokenToGun.js new file mode 100644 index 0000000..87ce126 --- /dev/null +++ b/saveApiTokenToGun.js @@ -0,0 +1,19 @@ +const HttpError = require('./HttpError'); + +const saveApiTokenToGun = (walletAddress, encryptedToken) => { + return new Promise((resolve, reject) => { + require('./gundb') + .getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('api-tokens') + .put(encryptedToken, (ack) => { + if (ack.err) { + reject(new HttpError('Failed to save api token to Gun')); + } else { + resolve(); + } + }); + }); +}; + +module.exports = saveApiTokenToGun; diff --git a/server.js b/server.js index 625c775..9d7e43f 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,23 @@ const express = require('express'); -const { ethers, JsonRpcProvider, Contract } = require('ethers'); +const { ethers } = require('ethers'); const crypto = require('node:crypto'); const cors = require('cors'); -const puppeteer = require('puppeteer'); -const Whitelist = require('@synthetixio/synthetix-node-namespace/deployments/11155420/Whitelist'); -const Namespace = require('@synthetixio/synthetix-node-namespace/deployments/11155420/Namespace'); +const { getNamespaceContract, getWhitelistContract } = require('./contracts'); +const { getGun, decrypt } = require('./gundb'); //const Multicall3 = require('./Multicall3/11155420/Multicall3'); -const Gun = require('gun'); +const HttpError = require('./HttpError'); +const validateNamespace = require('./validateNamespace'); const jwt = require('jsonwebtoken'); +const { initGun } = require('./gundb'); const app = express(); require('dotenv').config(); -const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); +const { createProxyMiddleware } = require('http-proxy-middleware'); const basicAuth = require('basic-auth'); const { // PORT, UPSTREAM_IPFS_URL, - UPSTREAM_IPFS_CLUSTER_URL, GRAPH_API_ENDPOINT, } = require('./env'); @@ -32,38 +32,7 @@ setInterval(require('./updatePeers'), 60_000); setInterval(require('./updateStats'), 60_000); setInterval(require('./peersTracking'), 10_000); -class HttpError extends Error { - constructor(message, code) { - super(message); - this.code = code; - } -} - -class EthereumContractError extends Error { - constructor(message, originalError) { - super(message); - this.name = 'EthereumContractError'; - this.originalError = originalError; - } -} - -const generateHash = (data) => - crypto.createHash('sha256').update(`${data}:${process.env.SECRET}`).digest('hex'); - -const encrypt = async (data) => await Gun.SEA.encrypt(data, process.env.SECRET); -const decrypt = async (data) => await Gun.SEA.decrypt(data, process.env.SECRET); - -const getContract = (address, abi) => { - try { - const provider = new JsonRpcProvider('https://sepolia.optimism.io'); - return new Contract(address, abi, provider); - } catch (err) { - throw new EthereumContractError('Failed to get contract', err); - } -}; //const getMulticall3Contract = () => getContract(Multicall3.address, Multicall3.abi); -const getNamespaceContract = () => getContract(Namespace.address, Namespace.abi); -const getWhitelistContract = () => getContract(Whitelist.address, Whitelist.abi); const validateWalletAddress = (req, _res, next) => { if (!req.body.walletAddress) { @@ -102,7 +71,7 @@ const server = app.listen(PORT, () => { require('./updateStats')(); require('./peersTracking')(); }); -const gun = Gun({ web: server, file: process.env.GUNDB_STORAGE_PATH }); +initGun(server); const validateVerificationParameters = (req, _res, next) => { if (!req.body.nonce) { @@ -133,79 +102,13 @@ const verifyMessage = async (req, res, next) => { next(); }; -const createJwtToken = async (walletAddress) => { - return new Promise((resolve, reject) => { - jwt.sign({ walletAddress }, process.env.JWT_SECRET_KEY, { expiresIn: '1d' }, (err, token) => { - if (err) reject(err); - resolve(token); - }); - }); -}; - -const createJwtApiToken = async (walletAddress) => { - return new Promise((resolve, reject) => { - jwt.sign({ walletAddress }, process.env.JWT_SECRET_KEY, {}, (err, token) => { - if (err) reject(err); - resolve(token); - }); - }); -}; - -const saveTokenToGun = (walletAddress, encryptedToken) => { - return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('tokens') - .put(encryptedToken, (ack) => { - if (ack.err) { - reject(new HttpError('Failed to save token to Gun')); - } else { - resolve(); - } - }); - }); -}; - -const saveApiTokenToGun = (walletAddress, encryptedToken) => { - return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('api-tokens') - .put(encryptedToken, (ack) => { - if (ack.err) { - reject(new HttpError('Failed to save api token to Gun')); - } else { - resolve(); - } - }); - }); -}; - -app.post('/api/verify', validateVerificationParameters, verifyMessage, async (_req, res, next) => { - try { - const token = await createJwtToken(res.locals.address); - const encryptedToken = await encrypt(generateHash(token)); - await saveTokenToGun(res.locals.address, encryptedToken); - res.status(200).send({ token }); - } catch (err) { - next(err); - } -}); +app.post('/api/verify', validateVerificationParameters, verifyMessage, require('./routeApiVerify')); app.post( '/api/verify-api-token', validateVerificationParameters, verifyMessage, - async (_req, res, next) => { - try { - const apiToken = await createJwtApiToken(res.locals.address); - const encryptedApiToken = await encrypt(generateHash(apiToken)); - await saveApiTokenToGun(res.locals.address, encryptedApiToken); - res.status(200).send({ apiToken }); - } catch (err) { - next(err); - } - } + require('./routeApiVerifyApiToken') ); app.use( @@ -253,11 +156,11 @@ const verifyToken = (req) => { const validateTokenWithGun = (walletAddress, token) => { return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) + getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) .get('tokens') .once(async (tokenData) => { - if (!tokenData || (await decrypt(tokenData)) !== generateHash(token)) { + if (!tokenData || (await decrypt(tokenData)) !== require('./generateHash')(token)) { reject(new HttpError('Unauthorized', 401)); } else { resolve(); @@ -268,11 +171,11 @@ const validateTokenWithGun = (walletAddress, token) => { const validateApiTokenWithGun = (walletAddress, token) => { return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) + getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) .get('api-tokens') .once(async (tokenData) => { - if (!tokenData || (await decrypt(tokenData)) !== generateHash(token)) { + if (!tokenData || (await decrypt(tokenData)) !== require('./generateHash')(token)) { reject(new HttpError('Unauthorized', 401)); } else { resolve(); @@ -281,17 +184,6 @@ const validateApiTokenWithGun = (walletAddress, token) => { }); }; -const checkApiTokenWithGun = (walletAddress) => { - return new Promise((resolve) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('api-tokens') - .once((tokenData) => { - resolve(!!tokenData); - }); - }); -}; - const authenticateToken = async (req, _res, next) => { try { const decoded = await verifyToken(req); @@ -336,38 +228,6 @@ const verifyKeyGenNamespace = async (req, _res, next) => { } }; -const validateNamespace = (namespace, next) => { - const errors = []; - - if (!namespace) { - errors.push('Namespace cannot be empty.'); - } - - if (namespace && (namespace.length < 3 || namespace.length > 30)) { - errors.push('Namespace must be between 3 and 30 characters long.'); - } - - if (namespace && !/^[a-z0-9-_]+$/.test(namespace)) { - errors.push( - 'Namespace must be DNS-compatible: lowercase letters, numbers, dashes (-), or underscores (_).' - ); - } - - if (namespace && /^-|-$/.test(namespace)) { - errors.push('Namespace cannot start or end with a dash (-).'); - } - - if (namespace && /^_|_$/.test(namespace)) { - errors.push('Namespace cannot start or end with an underscore (_).'); - } - - if (errors.length > 0) { - return next(new HttpError(errors.join(' '), 400)); - } - - next(); -}; - const verifyNamePublishNamespace = async (req, _res, next) => { try { await validateNamespaceOwnership(req.query.key, req.user.walletAddress); @@ -383,160 +243,23 @@ app.get('/api/protected', authenticateToken, (_req, res) => { app.post('/api/generate-api-nonce', authenticateToken, createNonce); -app.get('/api/check-api-token', authenticateToken, async (req, res, next) => { - try { - res.status(200).json({ apiTokenGenerated: await checkApiTokenWithGun(req.user.walletAddress) }); - } catch (err) { - next(err); - } -}); - -const saveGeneratedKey = ({ walletAddress, key, id }) => { - return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('generated-keys') - .get(key) - .put({ key, id, published: false }, (ack) => { - if (ack.err) { - reject(new HttpError(`Failed to save ipns keys to Gun: ${ack.err}`)); - } else { - resolve(); - } - }); - }); -}; +app.get('/api/check-api-token', authenticateToken, require('./routeApiCheckApiToken')); app.use( '/api/v0/key/gen', authenticateToken, (req, _res, next) => validateNamespace(req.query.arg, next), verifyKeyGenNamespace, - createProxyMiddleware({ - target: `${UPSTREAM_IPFS_URL}/api/v0/key/gen`, - pathRewrite: { - '^/': '', - }, - selfHandleResponse: true, - on: { - proxyReq: function onProxyReq(proxyReq) { - proxyReq.removeHeader('authorization'); - }, - proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { - try { - res.removeHeader('trailer'); - if (proxyRes.statusCode < 400) { - await saveGeneratedKey({ - walletAddress: req.user.walletAddress, - key: req.query.arg, - id: JSON.parse(responseBuffer.toString('utf8')).Id, - }); - } - } catch (err) { - console.error('Error saving to Gun:', err.message); - } - return responseBuffer; - }), - }, - }) -); - -const deleteGeneratedKey = ({ walletAddress, key }) => { - return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('generated-keys') - .get(key) - .put(null, (ack) => { - if (ack.err) { - reject(new HttpError(`Failed to delete key: ${ack.err}`)); - } else { - resolve(); - } - }); - }); -}; - -app.use( - '/api/v0/key/rm', - authenticateToken, - verifyKeyGenNamespace, - createProxyMiddleware({ - target: `${UPSTREAM_IPFS_URL}/api/v0/key/rm`, - pathRewrite: { - '^/': '', - }, - selfHandleResponse: true, - on: { - proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { - try { - res.removeHeader('trailer'); - if (proxyRes.statusCode < 400) { - await Promise.all( - JSON.parse(responseBuffer.toString('utf8')).Keys.map((k) => - deleteGeneratedKey({ - walletAddress: req.user.walletAddress, - key: k.Name, - }) - ) - ); - } - } catch (err) { - console.error('Error processing proxy response:', err); - } - return responseBuffer; - }), - }, - }) + require('./routeApiV0KeyGen') ); -const updateGeneratedKey = ({ walletAddress, key, updates }) => { - return new Promise((resolve, reject) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('generated-keys') - .get(key) - .put(updates, (ack) => { - if (ack.err) { - reject(new HttpError(`Failed to update key: ${ack.err}`)); - } else { - resolve(); - } - }); - }); -}; +app.use('/api/v0/key/rm', authenticateToken, verifyKeyGenNamespace, require('./routeApiV0KeyRm')); app.use( '/api/v0/name/publish', authenticateToken, verifyNamePublishNamespace, - createProxyMiddleware({ - target: `${UPSTREAM_IPFS_URL}/api/v0/name/publish`, - pathRewrite: { - '^/': '', - }, - selfHandleResponse: true, - on: { - proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { - try { - res.removeHeader('trailer'); - if (proxyRes.statusCode < 400) { - await updateGeneratedKey({ - walletAddress: req.user.walletAddress, - key: req.query.key, - updates: { - ipfs: JSON.parse(responseBuffer.toString('utf8')).Value, - published: true, - }, - }); - } - } catch (err) { - console.error('Error saving to Gun:', err.message); - } - return responseBuffer; - }), - }, - }) + require('./routeApiV0NamePublish') ); const authenticateAdmin = async (req, _res, next) => { @@ -610,16 +333,7 @@ app.get('/api/submitted-wallets', authenticateAdmin, async (_req, res, next) => } }); -app.post('/api/regenerate-api-token', authenticateToken, async (req, res, next) => { - try { - const newApiToken = await createJwtApiToken(req.user.walletAddress); - const encryptedNewApiToken = await encrypt(generateHash(newApiToken)); - await saveApiTokenToGun(req.user.walletAddress, encryptedNewApiToken); - res.status(200).send({ apiToken: newApiToken }); - } catch (err) { - next(err); - } -}); +app.post('/api/regenerate-api-token', authenticateToken, require('./routeApiRegenerateApiToken')); //const getNamespacesFromContract = async (walletAddress) => { // const NamespaceContract = getNamespaceContract(); @@ -697,64 +411,25 @@ app.post( '/api/unique-namespace', authenticateToken, (req, _res, next) => validateNamespace(req.body.namespace, next), - async (req, res, next) => { - try { - const NamespaceContract = getNamespaceContract(); - - const tokenId = await NamespaceContract.namespaceToTokenId(req.body.namespace); - if (!tokenId) { - res.status(200).json({ unique: true }); - return; - } - const owner = await NamespaceContract.ownerOf(tokenId); - if (owner.toLowerCase() === req.user.walletAddress.toLowerCase()) { - res.status(200).json({ unique: true }); - return; - } - if (owner.toLowerCase() !== req.user.walletAddress.toLowerCase()) { - res.status(200).json({ unique: false }); - return; - } - } catch (err) { - next(err); - } - } + require('./routeApiUniqueNamespace') ); -const checkGeneratedKey = async ({ walletAddress, key }) => { - return gun.get(generateHash(walletAddress.toLowerCase())).get('generated-keys').get(key); -}; - -app.post('/api/unique-generated-key', authenticateToken, async (req, res, next) => { - try { - const unique = await checkGeneratedKey({ - walletAddress: req.user.walletAddress, - key: req.body.key, - }); - - res.status(200).json({ unique: !unique }); - } catch (err) { - next(err); - } -}); - -const removeMetaData = (o) => { - const { _, ...withoutMeta } = o; - return withoutMeta; -}; +app.post('/api/unique-generated-key', authenticateToken, require('./routeApiUniqueGeneratedKey')); const getGeneratedKey = async (walletAddress) => { - const data = await gun.get(generateHash(walletAddress.toLowerCase())).get('generated-keys'); + const data = await getGun() + .get(require('./generateHash')(walletAddress.toLowerCase())) + .get('generated-keys'); if (!data) { return []; } return Promise.all( - Object.values(removeMetaData(data)) + Object.values(require('./removeMetaData')(data)) .filter((ref) => ref) .map(async (ref) => { - return removeMetaData(await gun.get(ref)); + return require('./removeMetaData')(await getGun().get(ref)); }) ); }; @@ -779,18 +454,7 @@ const verifyCidNamespace = async (req, _res, next) => { } }; -app.get('/api/cids', authenticateToken, verifyCidNamespace, async (req, res, next) => { - try { - res.status(200).json({ - cids: await getCidsFromGeneratedKey({ - walletAddress: req.user.walletAddress, - key: req.query.key, - }), - }); - } catch (err) { - next(err); - } -}); +app.get('/api/cids', authenticateToken, verifyCidNamespace, require('./routeApiCids')); const verifyRemoveCidNamespace = async (req, _res, next) => { try { @@ -804,77 +468,12 @@ const verifyRemoveCidNamespace = async (req, _res, next) => { } }; -app.post('/api/remove-cid', authenticateToken, verifyRemoveCidNamespace, async (req, res, next) => { - try { - const { cid, key } = req.body; - - if (!cid) { - return next(new HttpError('CID missed.', 400)); - } - - const response = await fetch(`${UPSTREAM_IPFS_CLUSTER_URL}/api/v0/pin/rm?arg=${cid}`, { - method: 'POST', - }); - if (!response.ok) { - throw new HttpError(`Failed to remove CID ${cid} from IPFS`); - } - - await deleteCidFromGeneratedKey({ - walletAddress: req.user.walletAddress, - key, - cid, - }); - - res.status(200).json({ success: true, cid }); - } catch (err) { - next(err); - } -}); - -const addCidToGeneratedKey = ({ walletAddress, key, cid }) => { - return new Promise((resolve) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('generated-keys') - .get(key) - .get('cids') - .get(cid) - .put({ cid }, resolve); - }); -}; - -const getCidsFromGeneratedKey = ({ walletAddress, key }) => { - return new Promise((resolve) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('generated-keys') - .get(key) - .get('cids') - .once((node) => { - if (!node) { - return resolve([]); - } - - const cids = Object.entries(removeMetaData(node)) - .filter(([_, value]) => value !== null) - .map(([key]) => key); - - resolve(cids); - }); - }); -}; - -const deleteCidFromGeneratedKey = ({ walletAddress, key, cid }) => { - return new Promise((resolve) => { - gun - .get(generateHash(walletAddress.toLowerCase())) - .get('generated-keys') - .get(key) - .get('cids') - .get(cid) - .put(null, resolve); - }); -}; +app.post( + '/api/remove-cid', + authenticateToken, + verifyRemoveCidNamespace, + require('./routeApiRemoveCid') +); app.use( '/api/v0/dag/import', @@ -887,36 +486,7 @@ app.use( }) ); -app.use( - '/api/v0/pin/add', - authenticateToken, - createProxyMiddleware({ - target: `${UPSTREAM_IPFS_CLUSTER_URL}/api/v0/pin/add`, - pathRewrite: { - '^/': '', - }, - selfHandleResponse: true, - on: { - proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { - try { - res.removeHeader('trailer'); - if (proxyRes.statusCode < 400) { - if (req.query.arg && req.query.customKey) { - await addCidToGeneratedKey({ - walletAddress: req.user.walletAddress, - key: req.query.customKey, - cid: req.query.arg, - }); - } - } - } catch (e) { - console.error(e); - } - return responseBuffer; - }), - }, - }) -); +app.use('/api/v0/pin/add', authenticateToken, require('./routeApiV0PinAdd')); app.use( '/api/v0/dag/get', @@ -929,41 +499,7 @@ app.use( }) ); -app.get('/api/screenshot', authenticateToken, async (req, res, next) => { - const { url } = req.query; - - if (!url) { - return next(new HttpError('URL parameter is required', 400)); - } - - const urlPattern = /^(https?:\/\/[a-zA-Z0-9.-]+(:\d+)?\/ipns\/[a-zA-Z0-9\/_-]+)$/; - if (!urlPattern.test(url)) { - return next(new HttpError('Invalid url', 400)); - } - - let browser; - try { - browser = await puppeteer.launch({ - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], - }); - const page = await browser.newPage(); - await page.setViewport({ width: 800, height: 600 }); - await page.goto(url, { waitUntil: 'networkidle2', timeout: 120000 }); - const screenshotBase64 = await page.screenshot({ - encoding: 'base64', - }); - await browser.close(); - - res.json({ image: `data:image/png;base64,${screenshotBase64}` }); - } catch { - next(new HttpError('Failed to generate screenshot', 500)); - } finally { - if (browser) { - await browser.close(); - } - } -}); +app.get('/api/screenshot', authenticateToken, require('./routeApiScreenshot')); app.use((err, _req, res, _next) => { const status = err.code || 500; diff --git a/validateNamespace.js b/validateNamespace.js new file mode 100644 index 0000000..0e5027f --- /dev/null +++ b/validateNamespace.js @@ -0,0 +1,35 @@ +const HttpError = require('./HttpError'); + +const validateNamespace = (namespace, next) => { + const errors = []; + + if (!namespace) { + errors.push('Namespace cannot be empty.'); + } + + if (namespace && (namespace.length < 3 || namespace.length > 30)) { + errors.push('Namespace must be between 3 and 30 characters long.'); + } + + if (namespace && !/^[a-z0-9-_]+$/.test(namespace)) { + errors.push( + 'Namespace must be DNS-compatible: lowercase letters, numbers, dashes (-), or underscores (_).' + ); + } + + if (namespace && /^-|-$/.test(namespace)) { + errors.push('Namespace cannot start or end with a dash (-).'); + } + + if (namespace && /^_|_$/.test(namespace)) { + errors.push('Namespace cannot start or end with an underscore (_).'); + } + + if (errors.length > 0) { + return next(new HttpError(errors.join(' '), 400)); + } + + next(); +}; + +module.exports = validateNamespace;