diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f3b28ff --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +GITHUB_APP_ID=your-github-app-id +GITHUB_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...your GH app private key here - 1 line...\n-----END PRIVATE KEY-----" +GITHUB_INSTALLATION_ID=your-github-app-installation-id +GITHUB_REPO_ISSUES_ENDPOINT=https://api.github.com/repos/allinbits/wallet-security-lists/issues diff --git a/.gitignore b/.gitignore index b512c09..1dcef2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +.env \ No newline at end of file diff --git a/functions/createissue/github-access.js b/functions/createissue/github-access.js new file mode 100644 index 0000000..ed0a70c --- /dev/null +++ b/functions/createissue/github-access.js @@ -0,0 +1,31 @@ +const fetch = require("node-fetch"); + +const privateKey = process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"); + +const generateJWT = () => { + const payload = { + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 600, + iss: process.env.GITHUB_APP_ID, + }; + return require("jsonwebtoken").sign(payload, privateKey, { algorithm: "RS256" }); +}; + +const getInstallationToken = async (installationId) => { + const jwtToken = generateJWT(); + + const response = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, { + method: "POST", + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: "application/vnd.github.v3+json", + }, + }); + + const data = await response.json(); + return data.token; +}; + +module.exports = { + getInstallationToken, +}; diff --git a/functions/createissue/index.js b/functions/createissue/index.js new file mode 100644 index 0000000..39407ed --- /dev/null +++ b/functions/createissue/index.js @@ -0,0 +1,53 @@ +const { getInstallationToken } = require("./github-access"); +const { fetchYamlTemplate, convertYamlToMarkdown, getSupportedTypes } = require("./issue-templates"); + +exports.handler = async function (event, context) { + const { type, data } = JSON.parse(event.body); + + try { + const supportedTypes = getSupportedTypes(); + + if (!supportedTypes.includes(type)) { + return { + statusCode: 400, + body: JSON.stringify({ message: "Issue type not recognized" }), + }; + } + + const token = await getInstallationToken(process.env.GITHUB_INSTALLATION_ID); + + const yamlContent = fetchYamlTemplate(`${type.toLowerCase()}-add.yaml`); + const issueBody = convertYamlToMarkdown(yamlContent, data); + + const response = await fetch(process.env.GITHUB_REPO_ISSUES_ENDPOINT, { + method: "POST", + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: `[${type}-add]: ${data.name}`, + body: issueBody, + }), + }); + + if (response.ok) { + return { + statusCode: 200, + body: JSON.stringify({ message: "Issue created successfully" }), + }; + } else { + const error = await response.json(); + return { + statusCode: response.status, + body: JSON.stringify({ message: "Error creating issue", error }), + }; + } + } catch (error) { + return { + statusCode: 500, + body: JSON.stringify({ message: "Internal server error", error: error.message }), + }; + } +}; diff --git a/functions/createissue/issue-templates.js b/functions/createissue/issue-templates.js new file mode 100644 index 0000000..701d16c --- /dev/null +++ b/functions/createissue/issue-templates.js @@ -0,0 +1,73 @@ +const fs = require("fs"); +const path = require("path"); +const yaml = require("js-yaml"); + +const readTemplateFiles = () => { + try { + const templateDir = path.join(__dirname, "..", "..", ".github", "ISSUE_TEMPLATE"); + const files = fs.readdirSync(templateDir); + return files.map((file) => path.join(templateDir, file)); + } catch (error) { + throw new Error(`Error while reading template files: ${error.message}`); + } +}; + +const getSupportedTypes = () => { + try { + const files = readTemplateFiles(); + + const supportedTypes = files + .map((filePath) => { + const file = path.basename(filePath); + const match = file.match(/(\w+)-add\.yaml/); + return match ? match[1].charAt(0).toUpperCase() + match[1].slice(1) : null; + }) + .filter(Boolean); + + return supportedTypes; + } catch (error) { + throw new Error(`Error while retrieving supported types: ${error.message}`); + } +}; + +const fetchYamlTemplate = (templateFile) => { + try { + const files = readTemplateFiles(); + + const templatePath = files.find((filePath) => filePath.endsWith(templateFile)); + if (!templatePath) { + throw new Error(`Template ${templateFile} not found`); + } + + const fileContent = fs.readFileSync(templatePath, "utf8"); + return fileContent; + } catch (error) { + throw new Error(`Error while retrieving the local template: ${error.message}`); + } +}; + +const convertYamlToMarkdown = (yamlContent, formData) => { + const parsedYaml = yaml.load(yamlContent); + let markdownContent = ""; + + parsedYaml.body.forEach((field) => { + markdownContent += `### ${field.attributes.label}\n`; + + if (field.type === "input" || field.type === "textarea") { + markdownContent += `${formData[field.id] || field.attributes.placeholder}\n\n`; + } else if (field.type === "dropdown") { + const selectedValue = formData[field.id]; + const defaultValue = field.attributes.options[0]; + const finalValue = field.attributes.options.includes(selectedValue) ? selectedValue : defaultValue; + markdownContent += `${finalValue}\n\n`; + } + }); + + return markdownContent; +}; + +module.exports = { + getSupportedTypes, + fetchYamlTemplate, + convertYamlToMarkdown, +}; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..56684c8 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,5 @@ +[build] + functions = "functions" + +[functions] + included_files = [".github/ISSUE_TEMPLATE/**"] \ No newline at end of file diff --git a/package.json b/package.json index 6dc5a00..6dcec58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wallet-security-lists", - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "index.js", "scripts": { @@ -10,6 +10,9 @@ "author": "", "license": "ISC", "dependencies": { + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^2.7.0", "octokit": "^4.0.2" } }