import React, {useState, useEffect} from 'react'; import { makeStyles } from '@material-ui/styles'; import {BrowserView, MobileView} from "react-device-detect"; import {Link} from 'react-router-dom'; import Paper from '@material-ui/core/Paper'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Button from '@material-ui/core/Button'; import Divider from '@material-ui/core/Divider'; import Select from '@material-ui/core/Select'; import MenuItem from '@material-ui/core/MenuItem'; import FormControl from '@material-ui/core/FormControl'; import Switch from '@material-ui/core/Switch'; import Dialog from '@material-ui/core/Dialog'; import DialogTitle from '@material-ui/core/DialogTitle'; import DialogContent from '@material-ui/core/DialogContent'; import DialogActions from '@material-ui/core/DialogActions'; import TextField from '@material-ui/core/TextField'; import Tooltip from '@material-ui/core/Tooltip'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; import Breadcrumbs from '@material-ui/core/Breadcrumbs'; import AppsIcon from '@material-ui/icons/Apps'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; import { useAlert } from "react-alert"; import words from "shellwords" const surfaceColor = "#27292D" const inputColor = "#383B40" const bodyDivStyle = { margin: "auto", width: "900px", } const actionListStyle = { paddingLeft: "10px", paddingRight: "10px", paddingBottom: "10px", paddingTop: "10px", marginTop: "5px", backgroundColor: inputColor, display: "flex", color: "white", } const boxStyle = { color: "white", flex: "1", marginLeft: "10px", marginRight: "10px", paddingLeft: "30px", paddingRight: "30px", paddingBottom: "30px", paddingTop: "30px", display: "flex", flexDirection: "column", backgroundColor: surfaceColor, } const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important" }, }) const rewrite = (args) => { return args.reduce(function(args, a){ if (0 === a.indexOf('-X')) { args.push('-X') args.push(a.slice(2)) } else { args.push(a) } return args }, []) } const parseField = (s) => { return s.split(/: (.+)/) } const isURL = (s) => { return /^https?:\/\//.test(s) } // Parses CURL to a real request const parseCurl = (s) => { //console.log("CURL: ", s) if (0 != s.indexOf('curl ')) { console.log("Not curl start") return "" } var args = rewrite(words.split(s)) var out = { method: 'GET', header: {} } var state = '' args.forEach(function(arg){ switch (true) { case isURL(arg): out.url = arg break; case arg === '-A' || arg === '--user-agent': state = 'user-agent' break; case arg === '-H' || arg === '--header': state = 'header' break; case arg === '-d' || arg === '--data' || arg === '--data-ascii': state = 'data' break; case arg === '-u' || arg === '--user': state = 'user' break; case arg === '-I' || arg === '--head': out.method = 'HEAD' break; case arg === '-X' || arg === '--request': state = 'method' break; case arg === '-b' || arg === '--cookie': state = 'cookie' break; case arg === '--compressed': out.header['Accept-Encoding'] = out.header['Accept-Encoding'] || 'deflate, gzip' break; case !!arg: switch (state) { case 'header': var field = parseField(arg) out.header[field[0]] = field[1] state = '' break; case 'user-agent': out.header['User-Agent'] = arg state = '' break; case 'data': if (out.method === 'GET' || out.method === 'HEAD') out.method = 'POST' out.header['Content-Type'] = out.header['Content-Type'] || 'application/x-www-form-urlencoded' out.body = out.body ? out.body + '&' + arg : arg state = '' break; case 'user': out.header['Authorization'] = 'Basic ' + btoa(arg) state = '' break; case 'method': out.method = arg state = '' break; case 'cookie': out.header['Set-Cookie'] = arg state = '' break; } break; } }) return out } // Should be different if logged in :| const AppCreator = (props) => { const { globalUrl, isLoaded } = props; const classes = useStyles(); const alert = useAlert() var upload = "" const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"] const actionBodyRequest = ["POST", "PUT", "PATCH",] const authenticationOptions = ["No authentication", "API key", "Bearer auth", "Basic auth", ] const apikeySelection = ["Header", "Query",] const [name, setName] = useState(""); const [contact, setContact] = useState(""); const [file, setFile] = useState(""); const [fileBase64, setFileBase64] = useState(""); const [isAppLoaded, setIsAppLoaded] = useState(false); const [isEditing, setIsEditing] = useState(false); const [description, setDescription] = useState(""); const [updater, setUpdater] = useState("tmp") const [baseUrl, setBaseUrl] = useState(""); const [actionsModalOpen, setActionsModalOpen] = useState(false); const [authenticationRequired, setAuthenticationRequired] = useState(false); const [authenticationOption, setAuthenticationOption] = useState(authenticationOptions[0]); const [parameterName, setParameterName] = useState(""); const [parameterLocation, setParameterLocation] = useState(apikeySelection.length > 0 ? apikeySelection[0] : ""); const [urlPath, setUrlPath] = useState(""); //const [urlPathQueries, setUrlPathQueries] = useState([{"name": "test", "required": false}]); const [urlPathQueries, setUrlPathQueries] = useState([]); const [urlPathParameters, ] = useState([]); const [firstrequest, setFirstrequest] = React.useState(true) const [, setBasedata] = React.useState({}) const [actions, setActions] = useState([]) const [errorCode, setErrorCode] = useState("") //const [actions, setActions] = useState([{ // "name": "Get workflows", // "description": "Get workflows", // "url": "/workflows", // "headers": "", // "queries": [], // "paths": [], // "body": "", // "errors": ["wutface", "WOAH"], // "method": actionNonBodyRequest[0], //}, { // "name": "Get workflow", // "description": "Get workflow", // "url": "/workflows/{id}", // "headers": "", // "queries": [], // "paths": ["id"], // "body": "", // "errors": ["wutface", "WOAH"], // "method": actionNonBodyRequest[0], //}, // //]) const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0]) const [currentAction, setCurrentAction] = useState({ "name": "", "description": "", "url": "", "headers": "", "paths": [], "queries": [], "body": "", "errors": [], "method": actionNonBodyRequest[0], }); useEffect(() => { if (firstrequest) { setFirstrequest(false) if (window.location.pathname.includes("apps/edit")) { setIsEditing(true) handleEditApp() } else { checkQuery() } } }) const handleEditApp = () => { fetch(globalUrl+"/api/v1/apps/"+props.match.params.appid+"/config", { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, credentials: "include", }) .then((response) => { if (response.status !== 200) { window.location.pathname = "/apps" } return response.json() }) .then((responseJson) => { if (!responseJson.success) { alert.error("Failed to get the app") setIsAppLoaded(true) } else { const data = JSON.parse(responseJson.body) parseIncomingOpenapiData(data) } }) .catch(error => { alert.error(error.toString()) }); } // Checks if there is an ID in the query, and gets it if it doesn't exist. const checkQuery = () => { var urlParams = new URLSearchParams(window.location.search) if (!urlParams.has("id")) { setIsAppLoaded(true) return } fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"), { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, credentials: "include", }) .then((response) => { if (response.status !== 200) { throw new Error("NOT 200 :O") } return response.json() }) .then((responseJson) => { setIsAppLoaded(true) if (!responseJson.success) { alert.error("Failed to verify") } else { const data = JSON.parse(responseJson.body) parseIncomingOpenapiData(data) } }) .catch(error => { alert.error(error.toString()) }); } const setFileFromb64 = () => { //const img = document.getElementById('logo') //var canvas = document.createElement('canvas') //var ctx = canvas.getContext('2d') //img.onload = function() { // console.log("LOADED?") // ctx.drawImage(img, 0, 0) // const canvasUrl = canvas.toDataURL() // console.log(canvasUrl) // setFileBase64(canvasUrl) //} } // Sets the data up as it should be at later points // This is the data FROM the database, not what's being saved const parseIncomingOpenapiData = (data) => { setBasedata(data) setName(data.info.title) setDescription(data.info.description) document.title = "Apps - "+data.info.title if (data.info !== null && data.info !== undefined && data.info["x-logo"] !== undefined) { setFileBase64(data.info["x-logo"]) } if (data.info.contact != undefined) { setContact(data.info.contact) } if (data.servers !== undefined && data.servers.length > 0) { var firstUrl = data.servers[0].url if (firstUrl.endsWith("/")) { setBaseUrl(firstUrl.slice(0, firstUrl.length-1)) } else { setBaseUrl(firstUrl) } } // This is annoying (: var securitySchemes = data.components.securityDefinitions if (securitySchemes === undefined) { securitySchemes = data.securitySchemes } if (securitySchemes === undefined) { securitySchemes = data.components.securitySchemes } // FIXME - headers? var newActions = [] var wordlist = {} for (let [path, pathvalue] of Object.entries(data.paths)) { for (let [method, methodvalue] of Object.entries(pathvalue)) { var newaction = { "name": methodvalue.summary, "description": methodvalue.description, "url": path, "method": method.toUpperCase(), "headers": "", "queries": [], "paths": [], "body": "", "errors": [], } for (var key in methodvalue.parameters) { const parameter = methodvalue.parameters[key] if (parameter.in === "query") { var tmpaction = { "description": parameter.description, "name": parameter.name, "required": parameter.required, "in": "query", } if (parameter.required === undefined) { tmpaction.required = false } newaction.queries.push(tmpaction) } else if (parameter.in === "path") { // FIXME - parse this to the URL too newaction.paths.push(parameter.name) // FIXME: This doesn't follow OpenAPI3 exactly. // https://swagger.io/docs/specification/describing-request-body/ // https://swagger.io/docs/specification/describing-parameters/ // Need to split the data. } else if (parameter.in === "body") { console.log("BODY: ", parameter) newaction.body = parameter.example } else if (parameter.in === "header") { newaction.headers += `${parameter.name}=${parameter.example}\n` } } if (newaction.name === "" || newaction.name === undefined) { // Find a unique part of the string // FIXME: Looks for length between /, find the one where they differ // Should find others with the same START to their path // Make a list of reserved names? Aka things that show up only once if (Object.getOwnPropertyNames(wordlist).length === 0) { for (let [newpath, pathvalue] of Object.entries(data.paths)) { const newpathsplit = newpath.split("/") for(var key in newpathsplit) { const pathitem = newpathsplit[key].toLowerCase() if (wordlist[pathitem] === undefined) { wordlist[pathitem] = 1 } else { wordlist[pathitem] += 1 } } } } //console.log("WORDLIST: ", wordlist) // Remove underscores and make it normal with upper case etc const urlsplit = path.split("/") if (urlsplit.length > 0) { var curname = "" for(var key in urlsplit) { var subpath = urlsplit[key] if (wordlist[subpath] > 2 || subpath.length < 1) { continue } curname = subpath break } // FIXME: If name exists, // FIXME: Check if first part of parsedname is verb, otherwise use method const parsedname = curname.split("_").join(" ").split("-").join(" ").split("{").join(" ").split("}").join(" ").trim() if (parsedname.length === 0) { newaction.errors.push("Missing name") } else { const newname = method.charAt(0).toUpperCase() + method.slice(1) + " " + parsedname const searchactions = newActions.find(data => data.name === newname) console.log("SEARCH: ", searchactions) if (searchactions !== undefined) { newaction.errors.push("Missing name") } else { newaction.name = newname } } } else { newaction.errors.push("Missing name") } } newActions.push(newaction) } } // FIXME: Have multiple authentication options? if (securitySchemes !== undefined) { for (const [key, value] of Object.entries(securitySchemes)) { if (value.scheme === "bearer") { setAuthenticationOption("Bearer auth") setAuthenticationRequired(true) break } else if (value.type === "apiKey") { setAuthenticationOption("API key") value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1); setParameterLocation(value.in) if (!apikeySelection.includes(value.in)) { console.log("APIKEY SELECT: ", apikeySelection) alert.error("Might be error in setting up API key authentication") } console.log("PARAM NAME: ", value.name) setParameterName(value.name) setAuthenticationRequired(true) break } else if (value.scheme === "basic") { setAuthenticationOption("Basic auth") setAuthenticationRequired(true) break } } } setActions(newActions) setIsAppLoaded(true) } // Saving the app that's been configured. const submitApp = () => { alert.info("Uploading and building app " + name) setErrorCode("") // Format the information const splitBase = baseUrl.split("/") const host = splitBase[2] const schemes = [splitBase[0]] const basePath = "/"+(splitBase.slice(3, )).join("/") const data = { "swagger": "3.0", "info": { "title": name, "description": description, "version": "1.0", "x-logo": fileBase64, }, "servers": [{"url": baseUrl}], "host": host, "basePath": basePath, "schemes": schemes, "paths": {}, "editing": isEditing, "components": { "securitySchemes": {}, }, "id": props.match.params.appid, "securityDefinitions": {}, } if (contact === "") { data.info["contact"] = { "name": "@frikkylikeme", "url": "https://twitter.com/frikkylikeme", "email": "frikky@shuffler.io", } } else { data.info["contact"] = contact } //console.log("LOADED IMAGE: ", data.image) for (var key in actions) { const item = actions[key] if (item.errors.length > 0) { alert.error("Saving with error in action "+item.name) } if (item.name === undefined && item.description !== undefined) { item.name = item.description } if (data.paths[item.url] === null || data.paths[item.url] === undefined) { data.paths[item.url] = {} } data.paths[item.url][item.method.toLowerCase()] = { "responses": { "default": { "description": "default", "schema": {} } }, "summary": item.name, "description": item.description, "parameters": [] } console.log("ACTION: ", item) if (item.queries.length > 0) { for (var querykey in item.queries) { const queryitem = item.queries[querykey] var newitem = { "in": "query", "name": queryitem.name, "description": "Generated by shuffler.io OpenAPI", "required": queryitem.required, "schema": { "type": "string", }, } if (queryitem.description !== undefined) { newitem.description = queryitem.description } data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) //console.log(queryitem) } } if (item.paths.length > 0) { for (querykey in item.paths) { const queryitem = item.paths[querykey] newitem = { "in": "path", "name": queryitem, "description": "Generated by shuffler.io OpenAPI", "required": true, "schema": { "type": "string", }, } if (queryitem.description !== undefined) { newitem.description = queryitem.description } data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) //console.log(queryitem) } } if (item.body !== undefined && item.body.length > 0) { const required = false newitem = { "in": "body", "name": "body", "multiline": true, "description": "Generated by shuffler.io OpenAPI", "required": required, "example": item.body, "schema": { "type": "string", }, } // FIXME - add application/json if JSON example? data.paths[item.url][item.method.toLowerCase()]["requestBody"] = { "description": "Generated by Shuffler.io", "required": required, "content": { "example": { "example": item.body, }, }, } data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) } if (item.headers.length > 0) { const required = false const headersSplit = item.headers.split("\n") for (var key in headersSplit) { const header = headersSplit[key] console.log("HEADER: ", header) var key = "" var value = "" if (header.length > 0 && header.includes("= ")) { const headersplit = header.split("= ") key = headersplit[0] value = headersplit[1] } else if (header.length > 0 && header.includes("=")) { const headersplit = header.split("=") key = headersplit[0] value = headersplit[1] } else if (header.length > 0 && header.includes(": ")) { const headersplit = header.split(": ") key = headersplit[0] value = headersplit[1] } else if (header.length > 0 && header.includes(":")) { const headersplit = header.split(":") key = headersplit[0] value = headersplit[1] } else { continue } if (key.length > 0 && value.length > 0) { newitem = { "in": "header", "name": key, "multiline": false, "description": "Header generated by shuffler.io OpenAPI", "required": false, "example": value, "schema": { "type": "string", }, } data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) } } } } if (authenticationOption === "API key") { if (parameterName.length === 0) { alert.error("A field name for the APIkey must be defined") return } data.components.securitySchemes["ApiKeyAuth"] = { "type": "apiKey", "in": parameterLocation.toLowerCase(), "name": parameterName, } } else if (authenticationOption === "Bearer auth") { data.components.securitySchemes["BearerAuth"] = { "type": "http", "scheme": "bearer", "bearerFormat": "UUID", } } else if (authenticationOption === "Basic auth") { data.components.securitySchemes["BasicAuth"] = { "type": "http", "scheme": "basic", } } console.log("ACTIONS: ", data.paths) fetch(globalUrl+"/api/v1/verify_openapi", { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify(data), credentials: "include", }) .then((response) => { //if (response.status !== 200) { // setErrorCode("An error occurred during validation") // throw new Error("NOT 200 :O") //} return response.json() }) .then((responseJson) => { if (!responseJson.success) { if (responseJson.reason !== undefined) { setErrorCode(responseJson.reason) alert.error("Failed to verify: "+responseJson.reason) } } else { alert.success("Successfully uploaded openapi") if (window.location.pathname.includes("/new")) { if (responseJson.id !== undefined && responseJson.id !== null) { window.location = `/apps/edit/${responseJson.id}` } } } }) .catch(error => { setErrorCode(error.toString()) alert.error(error.toString()) }); } const bearerAuth = authenticationOption === "Bearer auth" ?