Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api token #21

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions HttpError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
};
28 changes: 28 additions & 0 deletions contracts.js
Original file line number Diff line number Diff line change
@@ -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,
};
12 changes: 12 additions & 0 deletions createJwtApiToken.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions generateHash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const crypto = require('node:crypto');

module.exports = (data) =>
crypto.createHash('sha256').update(`${data}:${process.env.SECRET}`).digest('hex');
26 changes: 26 additions & 0 deletions gundb.js
Original file line number Diff line number Diff line change
@@ -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 };
4 changes: 4 additions & 0 deletions removeMetaData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = (o) => {
const { _, ...withoutMeta } = o;
return withoutMeta;
};
19 changes: 19 additions & 0 deletions routeApiCheckApiToken.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
34 changes: 34 additions & 0 deletions routeApiCids.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
12 changes: 12 additions & 0 deletions routeApiRegenerateApiToken.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
44 changes: 44 additions & 0 deletions routeApiRemoveCid.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
38 changes: 38 additions & 0 deletions routeApiScreenshot.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
};
20 changes: 20 additions & 0 deletions routeApiUniqueGeneratedKey.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
22 changes: 22 additions & 0 deletions routeApiUniqueNamespace.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
47 changes: 47 additions & 0 deletions routeApiV0KeyGen.js
Original file line number Diff line number Diff line change
@@ -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;
}),
},
});
47 changes: 47 additions & 0 deletions routeApiV0KeyRm.js
Original file line number Diff line number Diff line change
@@ -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;
}),
},
});
Loading