From d54c319a0389301953ae1f82b31e4397b63e1d83 Mon Sep 17 00:00:00 2001 From: Voktor Stolenets Date: Thu, 2 Jan 2025 14:25:14 +0200 Subject: [PATCH] Change: fix isAddress --- src/documentation.js | 367 +++++++++++++++++++++++++++++++++++++++++++ src/init.js | 57 +++++++ src/server.js | 312 ++++++++++++++++++++++++++++++++++++ src/server.ts | 11 +- 4 files changed, 742 insertions(+), 5 deletions(-) create mode 100644 src/documentation.js create mode 100644 src/init.js create mode 100644 src/server.js diff --git a/src/documentation.js b/src/documentation.js new file mode 100644 index 0000000..195d0fd --- /dev/null +++ b/src/documentation.js @@ -0,0 +1,367 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.docs = void 0; +exports.docs = `openapi: 3.0.3 +info: + title: AMBRodeo API + description: API documentation for AMBRodeo API + version: "1.0.0" +components: + headers: + SignatureHeader: + description: Signature for secret message. + schema: + type: string + AddressHeader: + description: Wallet address header used for authentication. + schema: + type: string + parameters: + GlobalSignatureHeader: + name: Signature + in: header + required: true + description: Signature for secret message. + schema: + type: string + GlobalAddressHeader: + name: Address + in: header + required: true + description: Wallet address header. + schema: + type: string + +paths: + /: + get: + summary: Root endpoint + responses: + "200": + description: Empty JSON response + content: + application/json: + schema: + type: object + /upload: + post: + summary: Upload a file + parameters: + - $ref: '#/components/parameters/GlobalSignatureHeader' + - $ref: '#/components/parameters/GlobalAddressHeader' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + "200": + description: File uploaded successfully + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + filename: + type: string + "400": + description: No file uploaded + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/user: + post: + summary: Add or update a user + parameters: + - $ref: '#/components/parameters/GlobalSignatureHeader' + - $ref: '#/components/parameters/GlobalAddressHeader' + requestBody: + content: + application/json: + schema: + type: object + properties: + userName: + type: string + image: + type: string + responses: + "200": + description: User added or updated + "400": + description: Invalid JSON payload + content: + application/json: + schema: + type: object + properties: + error: + type: string + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + get: + summary: Get user by address + parameters: + - $ref: '#/components/parameters/GlobalAddressHeader' + responses: + "200": + description: User data + content: + application/json: + schema: + type: object + properties: + address: + type: string + userName: + type: string + image: + type: string + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/message: + post: + summary: Add a message + parameters: + - $ref: '#/components/parameters/GlobalSignatureHeader' + - $ref: '#/components/parameters/GlobalAddressHeader' + requestBody: + content: + application/json: + schema: + type: object + properties: + tokenAddress: + type: string + message: + type: string + responses: + "200": + description: Message added successfully + "400": + description: Invalid JSON payload or missing required fields + content: + application/json: + schema: + type: object + properties: + error: + type: string + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/like: + post: + summary: Add or remove a like for a token + parameters: + - $ref: '#/components/parameters/GlobalSignatureHeader' + - $ref: '#/components/parameters/GlobalAddressHeader' + requestBody: + content: + application/json: + schema: + type: object + properties: + tokenAddress: + type: string + like: + type: boolean + responses: + "200": + description: Like added or removed successfully + "400": + description: Invalid JSON payload or missing required fields + content: + application/json: + schema: + type: object + properties: + error: + type: string + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/messages: + get: + summary: Get messages for a specific token + parameters: + - name: tokenAddress + in: query + required: true + schema: + type: string + - name: skip + in: query + required: false + schema: + type: integer + - name: limit + in: query + required: false + schema: + type: integer + responses: + "200": + description: List of messages + content: + application/json: + schema: + type: array + items: + type: object + properties: + address: + type: string + tokenAddress: + type: string + message: + type: string + timestamp: + type: string + format: date-time + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/token: + get: + summary: Get token by address + parameters: + - name: tokenAddress + in: query + required: true + schema: + type: string + responses: + "200": + description: Token data + content: + application/json: + schema: + type: object + properties: + tokenAddress: + type: string + like: + type: integer + timestamp: + type: string + format: date-time + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/likes: + get: + summary: Get likes for a user + parameters: + - $ref: '#/components/parameters/GlobalAddressHeader' + - name: skip + in: query + required: false + schema: + type: integer + - name: limit + in: query + required: false + schema: + type: integer + responses: + "200": + description: List of likes + content: + application/json: + schema: + type: array + items: + type: object + properties: + address: + type: string + tokenAddress: + type: string + timestamp: + type: string + format: date-time + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/secret: + get: + summary: Get a secret for the user address + parameters: + - $ref: '#/components/parameters/GlobalAddressHeader' + responses: + "200": + description: Secret generated + content: + application/json: + schema: + type: object + properties: + secret: + type: string + "500": + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string +`; diff --git a/src/init.js b/src/init.js new file mode 100644 index 0000000..f27e981 --- /dev/null +++ b/src/init.js @@ -0,0 +1,57 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getEnv = getEnv; +exports.initFastify = initFastify; +const dotenv = __importStar(require("dotenv")); +const fastify_1 = __importDefault(require("fastify")); +dotenv.config(); +function getEnv() { + const HOST = process.env.HOST || "localhost"; + const PORT = Number(process.env.PORT || 3000); + const DATABASE_URL = process.env.DATABASE_URL || "mongodb://localhost:27017/ambrodeo"; + const SUBGRAPHS_ENDPOINT = process.env.SUBGRAPHS_ENDPOINT || ""; + const UPLOAD_DIR = process.env.UPLOAD_DIR || __dirname; + return { HOST, PORT, DATABASE_URL, SUBGRAPHS_ENDPOINT, UPLOAD_DIR }; +} +function initFastify() { + return (0, fastify_1.default)({ + bodyLimit: 5000000, + logger: true, + }); +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..492e251 --- /dev/null +++ b/src/server.js @@ -0,0 +1,312 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const multipart_1 = __importDefault(require("@fastify/multipart")); +const mongodb_1 = __importDefault(require("@fastify/mongodb")); +const axios_1 = __importDefault(require("axios")); +const crypto_1 = __importDefault(require("crypto")); +const ethers_1 = __importDefault(require("ethers")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const js_yaml_1 = __importDefault(require("js-yaml")); +const swagger_1 = __importDefault(require("@fastify/swagger")); +const swagger_ui_1 = __importDefault(require("@fastify/swagger-ui")); +const init_1 = require("./init"); +const documentation_1 = require("./documentation"); +const query = `query GetToken($tokenAddress: ID!) {token(id: $tokenAddress) {id}}`; +const { HOST, PORT, DATABASE_URL, SUBGRAPHS_ENDPOINT, UPLOAD_DIR } = (0, init_1.getEnv)(); +const fastify = (0, init_1.initFastify)(); +const mapSecret = new Map(); +const startServer = async () => { + try { + const openApiSpec = js_yaml_1.default.load(documentation_1.docs); + fastify.register(swagger_1.default, { + openapi: openApiSpec, + }); + fastify.register(swagger_ui_1.default, { + routePrefix: "/docs", + uiConfig: { + docExpansion: "full", + deepLinking: false, + }, + }); + fastify.register(mongodb_1.default, { + forceClose: true, + url: DATABASE_URL, + }); + fastify.register(multipart_1.default); + fastify.addHook("preHandler", async (request, reply) => { + if (request.method != "POST") { + return; + } + try { + const { address, signature } = request.headers; + const secret = mapSecret.get(address); + if (!signature || !address || !secret) { + return reply.status(400).send({ + error: "Missing address or signature in headers", + }); + } + const recoveredAddress = ethers_1.default.verifyMessage(secret, signature); + if (recoveredAddress.toLowerCase() !== address.toLowerCase()) { + return reply.status(401).send({ error: "Invalid signature" }); + } + } + catch (error) { + request.log.error(error); + return reply.status(400).send({ error: error }); + } + }); + fastify.get("/", index); + fastify.post("/upload", uploadFile); + fastify.post("/api/user", addOrUpdateUser); + fastify.post("/api/message", addMessage); + fastify.post("/api/like", addOrDeleteLike); + fastify.get("/api/user", getUser); + fastify.get("/api/messages", getMessages); + fastify.get("/api/token", getToken); + fastify.get("/api/likes", getUserLikes); + fastify.get("/api/secret", getSecret); + await fastify.listen({ host: HOST, port: PORT }); + console.log(`Server running on http://${HOST}:${PORT}`); + } + catch (error) { + fastify.log.error(error); + process.exit(1); + } +}; +async function checkTokenExist(request, tokenAddress) { + try { + if ((await request.server.mongo.db + ?.collection("token") + .findOne({ tokenAddress })) != null) + return true; + const response = await axios_1.default.post(SUBGRAPHS_ENDPOINT, { query, variables: { tokenAddress } }, { + headers: { "Content-Type": "application/json" }, + }); + if (tokenAddress == response.data.data.token.id) { + await request.server.mongo.db?.collection("token").updateOne({ tokenAddress }, { + tokenAddress, + like: 0, + timestamp: new Date(), + }, { upsert: true }); + return true; + } + } + catch (error) { + request.log.error(error); + return false; + } +} +async function index(request, reply) { + return reply.send({}); +} +async function addMessage(request, reply) { + try { + const data = request.body; + if (!data || typeof data !== "object") { + return reply.status(400).send({ error: "Invalid JSON payload" }); + } + const { address } = request.headers; + let { tokenAddress, message } = data; + tokenAddress = tokenAddress.toLowerCase(); + if (!ethers_1.default.isAddress(tokenAddress)) + throw new Error("Invalid address"); + if (!checkTokenExist(request, tokenAddress)) + return reply.status(404).send({ token: "Token not found" }); + await request.server.mongo.db?.collection("massage").insertOne({ + address, + tokenAddress, + message: message, + timestamp: new Date(), + }); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } + return reply.send({}); +} +async function addOrUpdateUser(request, reply) { + try { + const data = request.body; + if (!data || typeof data !== "object") { + return reply.status(400).send({ error: "Invalid JSON payload" }); + } + const { address } = request.headers; + const { userName, image } = data; + await request.server.mongo.db?.collection("user").updateOne({ address }, { + address, + userName, + image, + timestamp: new Date(), + }, { upsert: true }); + } + catch (error) { + request.log.error(error); + return reply + .status(500) + .send({ error: error.message }); + } + return reply.send({}); +} +async function addOrDeleteLike(request, reply) { + try { + const data = request.body; + if (!data || typeof data !== "object") { + return reply.status(400).send({ error: "Invalid JSON payload" }); + } + const { address } = request.headers; + let { tokenAddress, like } = data; + tokenAddress = tokenAddress.toLowerCase(); + if (!ethers_1.default.isAddress(tokenAddress)) + throw new Error("Invalid address"); + if (!checkTokenExist(request, tokenAddress)) + return reply.status(404).send({ token: "Token not found" }); + if (like) { + const result = await request.server.mongo.db + ?.collection("like") + .updateOne({ address, tokenAddress }, { + address, + tokenAddress, + timestamp: new Date(), + }, { upsert: true }); + if (result.upsertedCount == 1) { + await request.server.mongo.db + ?.collection("token") + .updateOne({ tokenAddress }, { $inc: { like: 1 } }); + } + } + else { + const result = await request.server.mongo.db + ?.collection("token") + .deleteOne({ address, tokenAddress }); + if (result.deletedCount == 1) { + await request.server.mongo.db + ?.collection("token") + .updateOne({ tokenAddress }, { $inc: { like: -1 } }); + } + } + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } + return reply.send({}); +} +async function getUser(request, reply) { + try { + const { address } = request.headers; + const user = await request.server.mongo.db + ?.collection("user") + .findOne({ address }, { projection: { _id: 0 } }); + return reply.send(user); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } +} +async function getMessages(request, reply) { + try { + const { tokenAddress, skip, limit } = request.query; + const messages = await request.server.mongo.db + ?.collection("message") + .find({ tokenAddress }, { projection: { _id: 0 } }) + .sort({ timestamp: -1 }) + .skip(skip) + .limit(limit) + .toArray(); + return reply.send(messages); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } +} +async function getToken(request, reply) { + try { + const { tokenAddress } = request.query; + const token = await request.server.mongo.db + ?.collection("token") + .findOne({ tokenAddress }, { projection: { _id: 0 } }); + return reply.send(token); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } +} +async function getUserLikes(request, reply) { + try { + const { address } = request.headers; + const { skip, limit } = request.query; + const likes = await request.server.mongo.db + ?.collection("like") + .find({ address }, { projection: { _id: 0 } }) + .sort({ timestamp: -1 }) + .skip(skip) + .limit(limit) + .toArray(); + return reply.send(likes); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } +} +async function getSecret(request, reply) { + try { + let { address } = request.headers; + address = address.toLowerCase(); + console.log("----------------"); + console.log(address); + if (!ethers_1.default.isAddress(address)) + throw new Error("Invalid address"); + const secret = "AMBRodeo authorization secret: "; + secret.concat(crypto_1.default + .createHash("sha256") + .update(crypto_1.default.getRandomValues(new Uint8Array(32))) + .digest("hex")); + mapSecret.set(address, secret); + reply.send({ secret: secret }); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } +} +async function uploadFile(request, reply) { + try { + let { address } = request.headers; + address = address.toLowerCase(); + const data = await request.file(); + if (!data) { + reply.code(400).send({ error: "No file uploaded" }); + return; + } + const uploadDir = path_1.default.join(UPLOAD_DIR, "files", address); + if (!fs_1.default.existsSync(uploadDir)) { + fs_1.default.mkdirSync(uploadDir, { recursive: true }); + } + const filePath = path_1.default.join(uploadDir, data.filename); + await new Promise((resolve, reject) => { + const writeStream = fs_1.default.createWriteStream(filePath); + data.file.pipe(writeStream); + writeStream.on("finish", resolve); + writeStream.on("error", reject); + }); + reply.send({ + success: true, + message: "File uploaded", + filename: data.filename, + }); + } + catch (error) { + request.log.error(error); + return reply.status(500).send({ error: error.message }); + } +} +startServer(); diff --git a/src/server.ts b/src/server.ts index 77ae8da..b1c7a4b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,7 @@ import mongodb from "@fastify/mongodb"; import axios from "axios"; import crypto from "crypto"; import ethers from "ethers"; +import { isAddress } from "ethers"; import fs from "fs"; import path from "path"; import yaml from "js-yaml"; @@ -142,7 +143,7 @@ async function addMessage(request: FastifyRequest, reply: FastifyReply) { }; tokenAddress = tokenAddress.toLowerCase(); - if (!ethers.isAddress(tokenAddress)) throw new Error("Invalid address"); + if (!isAddress(tokenAddress)) throw new Error("Invalid address"); if (!checkTokenExist(request, tokenAddress)) return reply.status(404).send({ token: "Token not found" }); @@ -207,7 +208,7 @@ async function addOrDeleteLike(request: FastifyRequest, reply: FastifyReply) { }; tokenAddress = tokenAddress.toLowerCase(); - if (!ethers.isAddress(tokenAddress)) throw new Error("Invalid address"); + if (!isAddress(tokenAddress)) throw new Error("Invalid address"); if (!checkTokenExist(request, tokenAddress)) return reply.status(404).send({ token: "Token not found" }); @@ -319,10 +320,10 @@ async function getSecret(request: FastifyRequest, reply: FastifyReply) { try { let { address } = request.headers as { address?: string }; address = address.toLowerCase(); - if (!ethers.isAddress(address)) throw new Error("Invalid address"); + if (!isAddress(address)) throw new Error("Invalid address"); - const secret = "AMBRodeo authorization secret: "; - secret.concat( + let secret = "AMBRodeo authorization secret: "; + secret = secret.concat( crypto .createHash("sha256") .update(crypto.getRandomValues(new Uint8Array(32)))