3783 lines
115 KiB
React
3783 lines
115 KiB
React
import React, {useState, useEffect} from 'react';
|
||
import { makeStyles } from '@material-ui/styles';
|
||
import { useTheme } from '@material-ui/core/styles';
|
||
import {BrowserView, MobileView} from "react-device-detect";
|
||
|
||
import {Paper, Typography, FormControlLabel, Button, Divider, Select, MenuItem, FormControl, Switch, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Tooltip, Breadcrumbs, CircularProgress, Chip} from '@material-ui/core';
|
||
import {LockOpen as LockOpenIcon, FileCopy as FileCopyIcon, Delete as DeleteIcon, Remove as RemoveIcon, Add as AddIcon, CheckCircle as CheckCircleIcon, AttachFile as AttachFileIcon, Apps as AppsIcon, ErrorOutline as ErrorOutlineIcon} from '@material-ui/icons';
|
||
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
import {Link} from 'react-router-dom';
|
||
import YAML from 'yaml'
|
||
import ChipInput from 'material-ui-chip-input'
|
||
import { useAlert } from "react-alert";
|
||
import words from "shellwords"
|
||
|
||
import AvatarEditor from 'react-avatar-editor';
|
||
import AddAPhotoIcon from '@material-ui/icons/AddAPhoto';
|
||
import AddAPhotoOutlinedIcon from '@material-ui/icons/AddAPhotoOutlined';
|
||
import ZoomInOutlinedIcon from '@material-ui/icons/ZoomInOutlined';
|
||
import ZoomOutOutlinedIcon from '@material-ui/icons/ZoomOutOutlined';
|
||
import LoopIcon from '@material-ui/icons/Loop';
|
||
import AddPhotoAlternateIcon from '@material-ui/icons/AddPhotoAlternate';
|
||
|
||
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 dividerStyle = {
|
||
marginBottom: "10px",
|
||
marginTop: "10px",
|
||
height: "1px",
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}
|
||
|
||
const appIconStyle = {
|
||
marginLeft: "5px",
|
||
}
|
||
|
||
|
||
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 ""
|
||
}
|
||
|
||
try {
|
||
var args = rewrite(words.split(s))
|
||
} catch (e) {
|
||
return 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()
|
||
const theme = useTheme();
|
||
|
||
var upload = ""
|
||
const increaseAmount = 50
|
||
const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"]
|
||
const actionBodyRequest = ["POST", "PUT", "PATCH",]
|
||
const authenticationOptions = ["No authentication", "API key", "Bearer auth", "Basic auth", "Oauth2", "JWT"]
|
||
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 [newWorkflowTags, setNewWorkflowTags] = React.useState([]);
|
||
const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]);
|
||
const [parameterName, setParameterName] = useState("");
|
||
const [parameterLocation, setParameterLocation] = useState(apikeySelection.length > 0 ? apikeySelection[0] : "");
|
||
const [refreshUrl, setRefreshUrl] = useState("");
|
||
const [oauth2Scopes, setOauth2Scopes] = useState([]);
|
||
const [projectCategories, setProjectCategories] = useState([]);
|
||
const [selectedCategory, setSelectedCategory] = useState("");
|
||
|
||
const [urlPath, setUrlPath] = useState("");
|
||
//const [urlPathQueries, setUrlPathQueries] = useState([{"name": "test", "required": false}]);
|
||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||
const [update, setUpdate] = useState("")
|
||
const [urlPathParameters, ] = useState([]);
|
||
const [firstrequest, setFirstrequest] = React.useState(true)
|
||
const [basedata, setBasedata] = React.useState({})
|
||
const [actions, setActions] = useState([])
|
||
const [filteredActions, setFilteredActions] = useState([])
|
||
const [errorCode, setErrorCode] = useState("")
|
||
const [appBuilding, setAppBuilding] = useState(false)
|
||
const [extraBodyFields, setExtraBodyFields] = useState([])
|
||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false)
|
||
const [actionAmount, setActionAmount] = useState(increaseAmount)
|
||
const defaultAuth = {
|
||
"name": "",
|
||
"type": "header",
|
||
"example": "",
|
||
}
|
||
const [extraAuth, setExtraAuth] = useState([])
|
||
|
||
|
||
const [app, setApp] = useState({})
|
||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||
const [selectedAction, setSelectedAction] = useState({})
|
||
const [authLoaded, setAuthLoaded] = useState(false)
|
||
|
||
//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": "",
|
||
"file_field": "",
|
||
"description": "",
|
||
"url": "",
|
||
"headers": "",
|
||
"paths": [],
|
||
"queries": [],
|
||
"body": "",
|
||
"errors": [],
|
||
"example_response": "",
|
||
"method": actionNonBodyRequest[0],
|
||
});
|
||
|
||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
|
||
|
||
|
||
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 === false) {
|
||
alert.error("Failed to get the app")
|
||
setIsAppLoaded(true)
|
||
window.location.pathname = "/search"
|
||
} else {
|
||
parseIncomingOpenapiData(responseJson)
|
||
}
|
||
})
|
||
.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")) {
|
||
setActionAmount(0)
|
||
|
||
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{
|
||
parseIncomingOpenapiData(responseJson)
|
||
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.log("Error: ", error.toString())
|
||
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)
|
||
//}
|
||
}
|
||
|
||
|
||
const handleGetRef = (parameter, data) => {
|
||
try {
|
||
if (parameter === null || parameter["$ref"] === undefined) {
|
||
//console.log("$ref not found in getref for: ", parameter)
|
||
return parameter
|
||
}
|
||
} catch (e) {
|
||
console.log("Failed getting $ref of ", parameter)
|
||
return parameter
|
||
}
|
||
|
||
const paramsplit = parameter["$ref"].split("/")
|
||
if (paramsplit[0] !== "#") {
|
||
console.log("Bad param: ", paramsplit)
|
||
return parameter
|
||
}
|
||
|
||
var newitem = data
|
||
for (var key in paramsplit) {
|
||
var tmpparam = paramsplit[key]
|
||
if (tmpparam === "#") {
|
||
continue
|
||
}
|
||
|
||
if (newitem[tmpparam] === undefined) {
|
||
return parameter
|
||
}
|
||
|
||
newitem = newitem[tmpparam]
|
||
}
|
||
|
||
return newitem
|
||
}
|
||
|
||
const base64_decode = (str) => {
|
||
return decodeURIComponent(atob(str).split('').map(function(c) {
|
||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||
}).join(''))
|
||
}
|
||
|
||
// 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) => {
|
||
const parsedapp = data.openapi === undefined ? data : JSON.parse(base64_decode(data.openapi))
|
||
data = parsedapp.body === undefined ? parsedapp : parsedapp.body
|
||
|
||
var jsonvalid = false
|
||
var tmpvalue = ""
|
||
try {
|
||
data = JSON.parse(data)
|
||
jsonvalid = true
|
||
} catch (e) {
|
||
console.log("Error JSON: ", e)
|
||
}
|
||
|
||
if (!jsonvalid) {
|
||
try {
|
||
data = YAML.parse(data)
|
||
jsonvalid = true
|
||
} catch(e) {
|
||
console.log("Error YAML: ", e)
|
||
}
|
||
}
|
||
|
||
if (!jsonvalid) {
|
||
alert.info("OpenAPI data is invalid.")
|
||
return
|
||
}
|
||
|
||
setBasedata(data)
|
||
if (data.info !== null && data.info !== undefined) {
|
||
if (data.info.title !== undefined && data.info.title !== null) {
|
||
if (data.info.title.length > 29) {
|
||
setName(data.info.title.slice(0, 29))
|
||
} else {
|
||
setName(data.info.title)
|
||
}
|
||
}
|
||
|
||
setDescription(data.info.description)
|
||
document.title = "Apps - "+data.info.title
|
||
|
||
if (data.info["x-logo"] !== undefined) {
|
||
|
||
if (data.info["x-logo"].url !== undefined) {
|
||
console.log("PARSED LOGO: ", data.info["x-logo"].url)
|
||
setFileBase64(data.info["x-logo"].url)
|
||
} else {
|
||
setFileBase64(data.info["x-logo"])
|
||
}
|
||
console.log("")
|
||
console.log("")
|
||
console.log("LOGO: ", data.info["x-logo"])
|
||
console.log("")
|
||
console.log("")
|
||
}
|
||
|
||
if (data.info.contact !== undefined) {
|
||
setContact(data.info.contact)
|
||
}
|
||
|
||
if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) {
|
||
if (typeof(data.info["x-categories"]) == "array") {
|
||
} else {
|
||
|
||
}
|
||
setNewWorkflowCategories(data.info["x-categories"])
|
||
}
|
||
}
|
||
|
||
if (data.tags !== undefined && data.tags.length > 0) {
|
||
var newtags = []
|
||
for (var key in data.tags) {
|
||
if (data.tags[key].name.length > 50) {
|
||
console.log("Skipping tag cus it's too long: ", data.tags[key].name.length)
|
||
continue
|
||
}
|
||
|
||
newtags.push(data.tags[key].name)
|
||
}
|
||
|
||
if (newtags.length > 10) {
|
||
newtags = newtags.slice(0,9)
|
||
}
|
||
|
||
setNewWorkflowTags(newtags)
|
||
}
|
||
|
||
// This is annoying (:
|
||
var securitySchemes = data.components.securityDefinitions
|
||
if (securitySchemes === undefined) {
|
||
securitySchemes = data.securitySchemes
|
||
}
|
||
|
||
if (securitySchemes === undefined) {
|
||
securitySchemes = data.components.securitySchemes
|
||
}
|
||
|
||
const allowedfunctions = [
|
||
"GET",
|
||
"CONNECT",
|
||
"HEAD",
|
||
"DELETE",
|
||
"POST",
|
||
"PATCH",
|
||
"PUT",
|
||
]
|
||
|
||
var newActions = []
|
||
var wordlist = {}
|
||
var all_categories = []
|
||
if (data.paths !== null && data.paths !== undefined) {
|
||
for (let [path, pathvalue] of Object.entries(data.paths)) {
|
||
for (let [method, methodvalue] of Object.entries(pathvalue)) {
|
||
if (methodvalue === null) {
|
||
alert.info("Skipped method (null)"+method)
|
||
continue
|
||
}
|
||
|
||
if (!allowedfunctions.includes(method.toUpperCase())) {
|
||
// Typical YAML issue
|
||
if (method !== "parameters") {
|
||
console.log("Invalid method: ", method, "data: ", methodvalue)
|
||
alert.info("Skipped method (not allowed): "+method)
|
||
}
|
||
continue
|
||
}
|
||
|
||
var tmpname = methodvalue.summary
|
||
if (methodvalue.operationId !== undefined && methodvalue.operationId !== null && methodvalue.operationId.length > 0 && (tmpname === undefined || tmpname.length === 0)) {
|
||
tmpname = methodvalue.operationId
|
||
}
|
||
|
||
tmpname = tmpname.replaceAll(".", " ")
|
||
|
||
var newaction = {
|
||
"name": tmpname,
|
||
"description": methodvalue.description,
|
||
"url": path,
|
||
"file_field": "",
|
||
"method": method.toUpperCase(),
|
||
"headers": "",
|
||
"queries": [],
|
||
"paths": [],
|
||
"body": "",
|
||
"errors": [],
|
||
"example_response": "",
|
||
}
|
||
|
||
// Finding category
|
||
if (path.includes("/")) {
|
||
const pathsplit = path.split("/")
|
||
var categoryindex = -1
|
||
// Stupid way of finding a category/grouping
|
||
for (var key in pathsplit) {
|
||
if (pathsplit[key].length > 0 && pathsplit[key] !== "v1" && pathsplit[key] !== "v2" && pathsplit[key] !== "api" && pathsplit[key] !== "1.0" && pathsplit[key] !== "apis") {
|
||
newaction["category"] = pathsplit[key]
|
||
if (!all_categories.includes(pathsplit[key])) {
|
||
all_categories.push(pathsplit[key])
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Typescript? I think not ;)
|
||
if (methodvalue["requestBody"] !== undefined) {
|
||
//console.log("Handle requestbody: ", methodvalue["requestBody"])
|
||
if (methodvalue["requestBody"]["content"] !== undefined) {
|
||
if (methodvalue["requestBody"]["content"]["application/json"] !== undefined) {
|
||
newaction["headers"] = "Content-Type=application/json\nAccept=application/json"
|
||
if (methodvalue["requestBody"]["content"]["application/json"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["application/json"]["schema"] !== null) {
|
||
if (methodvalue["requestBody"]["content"]["application/json"]["schema"]["properties"] !== undefined) {
|
||
var tmpobject = {}
|
||
for (let [prop, propvalue] of Object.entries(methodvalue["requestBody"]["content"]["application/json"]["schema"]["properties"])) {
|
||
tmpobject[prop] = `\$\{${prop}\}`
|
||
}
|
||
|
||
//console.log("Data: ", data)
|
||
for (var subkey in methodvalue["requestBody"]["content"]["application/json"]["schema"]["required"]) {
|
||
const tmpitem = methodvalue["requestBody"]["content"]["application/json"]["schema"]["required"][subkey]
|
||
tmpobject[tmpitem] = `\$\{${tmpitem}\}`
|
||
}
|
||
|
||
newaction["body"] = JSON.stringify(tmpobject, null, 2)
|
||
} else if (methodvalue["requestBody"]["content"]["application/json"]["schema"]["$ref"] !== undefined && methodvalue["requestBody"]["content"]["application/json"]["schema"]["$ref"] !== null) {
|
||
const retRef = handleGetRef(methodvalue["requestBody"]["content"]["application/json"]["schema"], data)
|
||
var newbody = {}
|
||
// Can handle default, required, description and type
|
||
for (var propkey in retRef.properties) {
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
|
||
newbody[parsedkey] = "${"+parsedkey+"}"
|
||
}
|
||
|
||
newaction["body"] = JSON.stringify(newbody, null, 2)
|
||
}
|
||
}
|
||
} else if (methodvalue["requestBody"]["content"]["application/xml"] !== undefined) {
|
||
console.log("METHOD XML: ", methodvalue)
|
||
newaction["headers"] = "Content-Type=application/xml\nAccept=application/xml"
|
||
if (methodvalue["requestBody"]["content"]["application/xml"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["application/xml"]["schema"] !== null) {
|
||
if (methodvalue["requestBody"]["content"]["application/xml"]["schema"]["properties"] !== undefined) {
|
||
var tmpobject = {}
|
||
for (let [prop, propvalue] of Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["properties"])) {
|
||
tmpobject[prop] = `\$\{${prop}\}`
|
||
}
|
||
|
||
for (var subkey in methodvalue["requestBody"]["content"]["application/xml"]["schema"]["required"]) {
|
||
const tmpitem = methodvalue["requestBody"]["content"]["application/xml"]["schema"]["required"][subkey]
|
||
tmpobject[tmpitem] = `\$\{${tmpitem}\}`
|
||
}
|
||
|
||
//console.log("OBJ XML: ", tmpobject)
|
||
//newaction["body"] = XML.stringify(tmpobject, null, 2)
|
||
}
|
||
}
|
||
} else {
|
||
//console.log("REQUESTBODY: ", methodvalue["requestBody"]["content"])
|
||
if (methodvalue["requestBody"]["content"]["example"] !== undefined) {
|
||
if (methodvalue["requestBody"]["content"]["example"]["example"] !== undefined) {
|
||
newaction["body"] = methodvalue["requestBody"]["content"]["example"]["example"]
|
||
//JSON.stringify(tmpobject, null, 2)
|
||
}
|
||
} else if (methodvalue["requestBody"]["content"]["multipart/form-data"] !== undefined) {
|
||
if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"] !== null) {
|
||
if (methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["type"] === "object") {
|
||
const fieldname = methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"]["fieldname"]
|
||
if (fieldname !== undefined) {
|
||
console.log("FIELDNAME: ", fieldname)
|
||
newaction.file_field = fieldname["value"]
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
|
||
var schemas = []
|
||
const content = methodvalue["requestBody"]["content"]
|
||
if (content !== undefined && content !== null) {
|
||
//console.log("CONTENT: ", content)
|
||
for (const [subkey, subvalue] of Object.entries(content)) {
|
||
if (subvalue["schema"] !== undefined) {
|
||
//console.log("SCHEMA: ", subvalue["schema"])
|
||
if (subvalue["schema"]["$ref"] !== undefined) {
|
||
//console.log("SCHEMA FOUND REF!")
|
||
|
||
if (!schemas.includes(subvalue["schema"]["$ref"])) {
|
||
schemas.push(subvalue["schema"]["$ref"])
|
||
}
|
||
}
|
||
} else {
|
||
console.log("ERROR: couldn't find schema for ", subvalue, method)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (schemas.length === 1) {
|
||
const parameter = handleGetRef({"$ref": schemas[0]}, data)
|
||
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {}
|
||
for (var propkey in parameter.properties) {
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
|
||
if (parameter.properties[propkey].type === undefined) {
|
||
console.log("Skipping (4): ", parameter.properties[propkey])
|
||
continue
|
||
}
|
||
|
||
if (parameter.properties[propkey].type === "string") {
|
||
if (parameter.properties[propkey].description !== undefined) {
|
||
newbody[parsedkey] = parameter.properties[propkey].description
|
||
} else {
|
||
newbody[parsedkey] = ""
|
||
}
|
||
} else if (parameter.properties[propkey].type.includes("int") || parameter.properties[propkey].type.includes("uint64")) {
|
||
newbody[parsedkey] = 0
|
||
} else if (parameter.properties[propkey].type.includes("boolean")) {
|
||
newbody[parsedkey] = false
|
||
} else if (parameter.properties[propkey].type.includes("array")) {
|
||
newbody[parsedkey] = []
|
||
} else {
|
||
console.log("CANT HANDLE JSON TYPE (4)", parameter.properties[propkey].type, parameter.properties[propkey])
|
||
newbody[parsedkey] = []
|
||
}
|
||
}
|
||
|
||
newaction["body"] = JSON.stringify(newbody, null, 2)
|
||
} else {
|
||
console.log("CANT HANDLE PARAM: (4) ", parameter.properties)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// HAHAHA wtf is this.
|
||
if (methodvalue.responses !== undefined && methodvalue.responses !== null) {
|
||
if (methodvalue.responses.default !== undefined) {
|
||
if (methodvalue.responses.default.content !== undefined) {
|
||
if (methodvalue.responses.default.content["text/plain"] !== undefined) {
|
||
if (methodvalue.responses.default.content["text/plain"]["schema"] !== undefined) {
|
||
if (methodvalue.responses.default.content["text/plain"]["schema"]["example"] !== undefined) {
|
||
newaction.example_response = methodvalue.responses.default.content["text/plain"]["schema"]["example"]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
var selectedReturn = ""
|
||
if (methodvalue.responses["200"] !== undefined) {
|
||
selectedReturn = "200"
|
||
} else if (methodvalue.responses["201"] !== undefined) {
|
||
selectedReturn = "201"
|
||
}
|
||
|
||
// Parsing examples. This should be standardized lol
|
||
if (methodvalue.responses[selectedReturn] !== undefined) {
|
||
const selectedExample = methodvalue.responses[selectedReturn]
|
||
if (selectedExample["content"] !== undefined) {
|
||
if (selectedExample["content"]["application/json"] !== undefined) {
|
||
if (selectedExample["content"]["application/json"]["schema"] !== undefined && selectedExample["content"]["application/json"]["schema"] !== null) {
|
||
if (selectedExample["content"]["application/json"]["schema"]["$ref"] !== undefined) {
|
||
//console.log("REF EXAMPLE: ", selectedExample["content"]["application/json"]["schema"])
|
||
const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"], data)
|
||
//console.log("GOT REF RETURN AS EXAMPLE: ", parameter)
|
||
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {}
|
||
for (var propkey in parameter.properties) {
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
|
||
if (parameter.properties[propkey].type === undefined) {
|
||
console.log("Skipping (1): ", parameter.properties[propkey])
|
||
continue
|
||
}
|
||
|
||
if (parameter.properties[propkey].type === "string") {
|
||
if (parameter.properties[propkey].description !== undefined) {
|
||
newbody[parsedkey] = parameter.properties[propkey].description
|
||
} else {
|
||
newbody[parsedkey] = ""
|
||
}
|
||
} else if (parameter.properties[propkey].type.includes("int")) {
|
||
newbody[parsedkey] = 0
|
||
} else if (parameter.properties[propkey].type.includes("boolean")) {
|
||
newbody[parsedkey] = false
|
||
} else if (parameter.properties[propkey].type.includes("array")) {
|
||
//console.log("Added empty array. Base is: ", parameter.properties[propkey].type)
|
||
|
||
//const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"], data)
|
||
newbody[parsedkey] = []
|
||
} else {
|
||
console.log("CANT HANDLE JSON TYPE ", parameter.properties[propkey].type, parameter.properties[propkey])
|
||
newbody[parsedkey] = []
|
||
}
|
||
}
|
||
newaction.example_response = JSON.stringify(newbody, null, 2)
|
||
} else {
|
||
console.log("CANT HANDLE PARAM: (1) ", parameter.properties)
|
||
}
|
||
} else {
|
||
// Just selecting the first one. bleh.
|
||
if (selectedExample["content"]["application/json"]["schema"]["allOf"] !== undefined) {
|
||
//console.log("ALLOF: ", selectedExample["content"]["application/json"]["schema"]["allOf"])
|
||
//console.log("BAD EXAMPLE: (SKIP ALLOF) ", selectedExample["content"]["application/json"]["schema"]["allOf"])
|
||
var selectedComponent = selectedExample["content"]["application/json"]["schema"]["allOf"]
|
||
if (selectedComponent.length >= 1) {
|
||
selectedComponent = selectedComponent[0]
|
||
|
||
const parameter = handleGetRef(selectedComponent, data)
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {}
|
||
for (var propkey in parameter.properties) {
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
|
||
if (parameter.properties[propkey].type === undefined) {
|
||
console.log("Skipping (2): ", parameter.properties[propkey])
|
||
continue
|
||
}
|
||
|
||
if (parameter.properties[propkey].type === "string") {
|
||
if (parameter.properties[propkey].description !== undefined) {
|
||
newbody[parsedkey] = parameter.properties[propkey].description
|
||
} else {
|
||
newbody[parsedkey] = ""
|
||
}
|
||
} else if (parameter.properties[propkey].type.includes("int")) {
|
||
newbody[parsedkey] = 0
|
||
} else if (parameter.properties[propkey].type.includes("boolean")) {
|
||
newbody[parsedkey] = false
|
||
} else {
|
||
console.log("CANT HANDLE JSON TYPE (2) ", parameter.properties[propkey].type)
|
||
newbody[parsedkey] = []
|
||
}
|
||
}
|
||
|
||
newaction.example_response = JSON.stringify(newbody, null, 2)
|
||
//newaction.example_response = JSON.stringify(parameter.properties, null, 2)
|
||
} else {
|
||
//newaction.example_response = parameter.properties
|
||
console.log("CANT HANDLE PARAM: (3) ", parameter.properties)
|
||
}
|
||
} else {
|
||
|
||
}
|
||
} else if (selectedExample["content"]["application/json"]["schema"]["properties"] !== undefined) {
|
||
if (selectedExample["content"]["application/json"]["schema"]["properties"]["data"] !== undefined) {
|
||
const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"]["properties"]["data"], data)
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {}
|
||
for (var propkey in parameter.properties) {
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase()
|
||
if (parameter.properties[propkey].type === undefined) {
|
||
console.log("Skipping (3): ", parameter.properties[propkey])
|
||
continue
|
||
}
|
||
|
||
if (parameter.properties[propkey].type === "string") {
|
||
if (parameter.properties[propkey].description !== undefined) {
|
||
newbody[parsedkey] = parameter.properties[propkey].description
|
||
} else {
|
||
newbody[parsedkey] = ""
|
||
}
|
||
console.log(parameter.properties[propkey])
|
||
} else if (parameter.properties[propkey].type.includes("int")) {
|
||
newbody[parsedkey] = 0
|
||
} else {
|
||
console.log("CANT HANDLE JSON TYPE (3) ", parameter.properties[propkey].type)
|
||
newbody[parsedkey] = []
|
||
}
|
||
}
|
||
|
||
newaction.example_response = JSON.stringify(newbody, null, 2)
|
||
//newaction.example_response = JSON.stringify(parameter.properties, null, 2)
|
||
} else {
|
||
//newaction.example_response = parameter.properties
|
||
console.log("CANT HANDLE PARAM: (3) ", parameter.properties)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for (var key in methodvalue.parameters) {
|
||
const parameter = handleGetRef(methodvalue.parameters[key], data)
|
||
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") {
|
||
// FIXME: Add tracking for components
|
||
// E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml
|
||
if (parameter.example !== undefined) {
|
||
newaction.body = parameter.example
|
||
}
|
||
} else if (parameter.in === "header") {
|
||
newaction.headers += `${parameter.name}=${parameter.example}\n`
|
||
} else {
|
||
console.log("WARNING: don't know how to handle this param: ", parameter)
|
||
}
|
||
}
|
||
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
if (data.servers !== undefined && data.servers.length > 0) {
|
||
var firstUrl = data.servers[0].url
|
||
if (firstUrl.includes("{") && firstUrl.includes("}") && data.servers[0].variables !== undefined) {
|
||
const regex = /{\w+}/g
|
||
const found = firstUrl.match(regex)
|
||
if (found !== null) {
|
||
for (var key in found) {
|
||
const item = found[key].slice(1, found[key].length-1)
|
||
const foundVar = data.servers[0].variables[item]
|
||
if (foundVar["default"] !== undefined) {
|
||
firstUrl = firstUrl.replace(found[key], foundVar["default"])
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
if (firstUrl.endsWith("/")) {
|
||
setBaseUrl(firstUrl.slice(0, firstUrl.length-1))
|
||
} else {
|
||
setBaseUrl(firstUrl)
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
//console.log("SECURITYSCHEMES: ", securitySchemes)
|
||
if (securitySchemes !== undefined) {
|
||
// FIXME: Should add Oauth2 (Microsoft) and JWT (Wazuh)
|
||
//console.log("SECURITY: ", securitySchemes)
|
||
//if (Object.entries(securitySchemes) > 1 &&
|
||
var newauth = []
|
||
for (const [key, value] of Object.entries(securitySchemes)) {
|
||
console.log(key, value)
|
||
if (key === "jwt") {
|
||
setAuthenticationOption("JWT")
|
||
setAuthenticationRequired(true)
|
||
|
||
if (value.in !== undefined && value.in !== null && value.in.length > 0) {
|
||
setParameterName(value.in)
|
||
}
|
||
} else if (value.scheme === "bearer") {
|
||
setAuthenticationOption("Bearer auth")
|
||
setAuthenticationRequired(true)
|
||
} else if (key === "Oauth2" || key === "Oauth2c") {
|
||
//alert.info("Can't handle Oauth2 auth yet.")
|
||
setAuthenticationOption("Oauth2")
|
||
setAuthenticationRequired(true)
|
||
|
||
//console.log("FLOW-1: ", value)
|
||
const flowkey = value.flow === undefined ? "flows" : "flow"
|
||
//console.log("FLOW: ", value[flowkey])
|
||
const basekey = value[flowkey].authorizationCode !== undefined ? "authorizationCode" : "implicit"
|
||
//console.log("FLOW2: ", value[flowkey][basekey])
|
||
if (value[flowkey] !== undefined && value[flowkey][basekey] !== undefined) {
|
||
if (value[flowkey][basekey].authorizationUrl !== undefined && parameterName.length === 0) {
|
||
setParameterName(value[flowkey][basekey].authorizationUrl)
|
||
}
|
||
|
||
var tokenUrl = ""
|
||
if (value[flowkey][basekey].tokenUrl !== undefined) {
|
||
setParameterLocation(value[flowkey][basekey].tokenUrl)
|
||
tokenUrl = value[flowkey][basekey].tokenUrl
|
||
} else {
|
||
setParameterLocation("")
|
||
}
|
||
|
||
if (value[flowkey][basekey].refreshUrl !== undefined) {
|
||
setRefreshUrl(value[flowkey][basekey].refreshUrl)
|
||
} else if (tokenUrl.length > 0) {
|
||
setRefreshUrl(tokenUrl)
|
||
}
|
||
|
||
if (value[flowkey][basekey].scopes !== undefined && value[flowkey][basekey].scopes !== null) {
|
||
if (value[flowkey][basekey].scopes.length > 0) {
|
||
setOauth2Scopes(value[flowkey][basekey].scopes)
|
||
} else {
|
||
var newscopes = []
|
||
for (let [scopekey, scopevalue] of Object.entries(value[flowkey][basekey].scopes)) {
|
||
if (scopekey.startsWith("http")) {
|
||
const scopekeysplit = scopekey.split("/")
|
||
if (scopekeysplit.length < 5) {
|
||
console.log("Skipping scope: ", scopekey)
|
||
alert.info("Skipping scope: "+scopekey)
|
||
continue
|
||
}
|
||
|
||
//console.log("Checking scope for: ", scopekey, scopekeysplit.length)
|
||
}
|
||
|
||
newscopes.push(scopekey)
|
||
}
|
||
|
||
setOauth2Scopes(newscopes)
|
||
}
|
||
}
|
||
} else {
|
||
console.log("Bad flowkey and basekey for oauth2: ", flowkey, basekey)
|
||
}
|
||
|
||
} else if (key === "ApiKeyAuth") {
|
||
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)
|
||
} else if (value.scheme === "basic") {
|
||
setAuthenticationOption("Basic auth")
|
||
setAuthenticationRequired(true)
|
||
} else if (value.scheme === "oauth2") {
|
||
setAuthenticationOption("Oauth2")
|
||
setAuthenticationRequired(true)
|
||
} else {
|
||
alert.error("Couldn't handle AUTH type: ", key)
|
||
//newauth.push({
|
||
// "name": key,
|
||
// "type": value.in,
|
||
// "example": "",
|
||
//})
|
||
}
|
||
}
|
||
|
||
if (newauth.length > 0) {
|
||
setExtraAuth(newauth)
|
||
}
|
||
}
|
||
|
||
if (newActions.length > increaseAmount-1) {
|
||
setActionAmount(increaseAmount)
|
||
} else {
|
||
setActionAmount(newActions.length)
|
||
}
|
||
|
||
//const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
|
||
if (newActions.length > 1000 && isCloud) {
|
||
alert.error("Cut down actions from "+newActions.length+" to 999 because of limit")
|
||
newActions = newActions.slice(0,999)
|
||
}
|
||
|
||
setProjectCategories(all_categories)
|
||
setActions(newActions)
|
||
setFilteredActions(newActions)
|
||
setIsAppLoaded(true)
|
||
}
|
||
|
||
// Saving the app that's been configured.
|
||
const submitApp = () => {
|
||
alert.info("Uploading and building app " + name)
|
||
setAppBuilding(true)
|
||
setErrorCode("")
|
||
|
||
// Format the information
|
||
const splitBase = baseUrl.split("/")
|
||
const host = splitBase[2]
|
||
const schemes = [splitBase[0]]
|
||
const basePath = "/"+(splitBase.slice(3, )).join("/")
|
||
|
||
const data = {
|
||
"openapi": "3.0.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,
|
||
}
|
||
|
||
if (basedata.info !== undefined && basedata.info.contact !== undefined) {
|
||
data.info["contact"] = basedata.info.contact
|
||
} else if (contact === "") {
|
||
data.info["contact"] = {
|
||
"name": "@frikkylikeme",
|
||
"url": "https://twitter.com/frikkylikeme",
|
||
"email": "frikky@shuffler.io",
|
||
}
|
||
} else {
|
||
data.info["contact"] = contact
|
||
}
|
||
|
||
if (newWorkflowTags.length > 0) {
|
||
var newtags = []
|
||
for (var key in newWorkflowTags) {
|
||
newtags.push({"name": newWorkflowTags[key]})
|
||
}
|
||
|
||
data["tags"] = newtags
|
||
}
|
||
|
||
if (newWorkflowCategories.length > 0) {
|
||
data["info"]["x-categories"] = newWorkflowCategories
|
||
}
|
||
|
||
// Handles actions
|
||
for (var key in actions) {
|
||
var 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] = {}
|
||
}
|
||
|
||
const regex = /[A-Za-z0-9 _]/g;
|
||
if (item.name === undefined) {
|
||
console.log("Skipping action ", item)
|
||
continue
|
||
}
|
||
|
||
const found = item.name.match(regex);
|
||
if (found !== null) {
|
||
item.name = found.join("")
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()] = {
|
||
"responses": {
|
||
"default": {
|
||
"description": "default",
|
||
"content": {
|
||
"text/plain": {
|
||
"schema": {
|
||
"type": "string",
|
||
"example": "",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
},
|
||
"summary": item.name,
|
||
"operationId": item.name.split(" ").join("_"),
|
||
"description": item.description,
|
||
"parameters": [],
|
||
"requestBody": {
|
||
"content": {
|
||
|
||
}
|
||
},
|
||
}
|
||
|
||
//console.log("ACTION: ", item)
|
||
|
||
if (item.example_response !== undefined && item.example_response.length > 0) {
|
||
// FIXME: Shallow copy of the string
|
||
var showResult = Object.assign("", item.example_response).trim()
|
||
showResult = showResult.split(" None").join(" \"None\"")
|
||
showResult = showResult.split("\'").join("\"")
|
||
showResult = showResult.split(" False").join(" false")
|
||
showResult = showResult.split(" True").join(" true")
|
||
|
||
var jsonvalid = true
|
||
try {
|
||
const tmp = String(JSON.parse(showResult))
|
||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||
jsonvalid = false
|
||
}
|
||
} catch (e) {
|
||
jsonvalid = false
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.type = "string"
|
||
if (jsonvalid) {
|
||
// FIXME: Add a JSON parser here - don't run it as a string.
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.example = showResult
|
||
|
||
} else {
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.example = item.example_response
|
||
|
||
}
|
||
}
|
||
|
||
if (item.queries.length > 0) {
|
||
var skipped = false
|
||
for (var querykey in item.queries) {
|
||
const queryitem = item.queries[querykey]
|
||
|
||
if (queryitem.name.toLowerCase() == "url") {
|
||
console.log(item.name+" uses a bad query: url")
|
||
continue
|
||
//skipped = true
|
||
//break
|
||
}
|
||
|
||
if (queryitem.name.toLowerCase() == "url"
|
||
|| queryitem.name.toLowerCase() == "body"
|
||
|| queryitem.name.toLowerCase() == "self"
|
||
|| queryitem.name.toLowerCase() == "ssl_verify"
|
||
|| queryitem.name.toLowerCase() == "queries"
|
||
|| queryitem.name.toLowerCase() == "headers"
|
||
|| queryitem.name.includes("[")
|
||
|| queryitem.name.includes("]")
|
||
|| queryitem.name.includes("{")
|
||
|| queryitem.name.includes("}")
|
||
|| queryitem.name.includes("(")
|
||
|| queryitem.name.includes(")")
|
||
|| queryitem.name.includes("!")
|
||
|| queryitem.name.includes("@")
|
||
|| queryitem.name.includes("#")
|
||
|| queryitem.name.includes("$")
|
||
|| queryitem.name.includes("%")
|
||
|| queryitem.name.includes("^")
|
||
|| queryitem.name.includes("&")
|
||
|| queryitem.name.includes(":")
|
||
|| queryitem.name.includes(";")
|
||
|| queryitem.name.includes("<")
|
||
|| queryitem.name.includes(">")
|
||
|| queryitem.name.includes("\"")
|
||
|| queryitem.name.includes("\'")
|
||
) {
|
||
console.log(item.name+" error: uses a bad query - not adding: ", queryitem.name)
|
||
continue
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
// Bad code as it doesn't allow for "anything".
|
||
if (skipped) {
|
||
alert.info("Bad configuration of "+item.name+". Skipping because queries are invalid.")
|
||
continue
|
||
}
|
||
}
|
||
//data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem)
|
||
|
||
if (item.paths.length > 0) {
|
||
for (querykey in item.paths) {
|
||
const queryitem = item.paths[querykey]
|
||
|
||
if (queryitem.toLowerCase() == "url") {
|
||
queryitem = "action_url"
|
||
}
|
||
|
||
if (queryitem.toLowerCase() == "apikey") {
|
||
queryitem = "action_apikey"
|
||
}
|
||
|
||
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)
|
||
}
|
||
} else {
|
||
// Always goes here if they didn't click anything :/
|
||
const values = getCurrentPaths(item.url)
|
||
const paths = values[0]
|
||
|
||
for (querykey in paths) {
|
||
const queryitem = 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 !== null && 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)
|
||
} else if (actionBodyRequest.includes(item.method.toUpperCase())) {
|
||
// Appending an empty field
|
||
const required = false
|
||
newitem = {
|
||
"in": "body",
|
||
"name": "body",
|
||
"multiline": true,
|
||
"description": "Generated by shuffler.io OpenAPI",
|
||
"required": required,
|
||
"example": "",
|
||
"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": "",
|
||
},
|
||
},
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem)
|
||
} else {
|
||
//console.log("Nothing to append?")
|
||
}
|
||
|
||
// https://swagger.io/docs/specification/describing-request-body/file-upload/
|
||
if (item.file_field !== undefined && item.file_field !== null && item.file_field.length > 0) {
|
||
console.log("HANDLE FILEFIELD SAVE: ", item.file_field)
|
||
data.paths[item.url][item.method.toLowerCase()]["requestBody"]["content"]["multipart/form-data"] = {
|
||
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"fieldname": {
|
||
"type": "string",
|
||
"value": item.file_field,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
console.log(data.paths[item.url][item.method.toLowerCase()]["requestBody"]["content"]["multipart/form-data"])
|
||
}
|
||
|
||
if (item.headers.length > 0) {
|
||
const required = false
|
||
|
||
const headersSplit = item.headers.split("\n")
|
||
for (var key in headersSplit) {
|
||
const header = headersSplit[key]
|
||
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 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")
|
||
setAppBuilding(false)
|
||
return
|
||
}
|
||
|
||
var newparamName = parameterName.replaceAll("\"", "")
|
||
newparamName = newparamName.replaceAll("\'", "")
|
||
|
||
data.components.securitySchemes["ApiKeyAuth"] = {
|
||
"type": "apiKey",
|
||
"in": parameterLocation.toLowerCase(),
|
||
"name": newparamName,
|
||
}
|
||
} else if (authenticationOption === "Bearer auth") {
|
||
data.components.securitySchemes["BearerAuth"] = {
|
||
"type": "http",
|
||
"scheme": "bearer",
|
||
"bearerFormat": "UUID",
|
||
}
|
||
} else if (authenticationOption === "JWT") {
|
||
data.components.securitySchemes["jwt"] = {
|
||
"type": "http",
|
||
"scheme": "bearer",
|
||
"bearerFormat": "JWT",
|
||
"in": parameterName,
|
||
}
|
||
|
||
console.log("SECURITYSCHEMES: ", data.components)
|
||
} else if (authenticationOption === "Basic auth") {
|
||
data.components.securitySchemes["BasicAuth"] = {
|
||
"type": "http",
|
||
"scheme": "basic",
|
||
}
|
||
} else if (authenticationOption === "Oauth2") {
|
||
var newparamName = parameterName.replaceAll("\"", "")
|
||
newparamName = newparamName.replaceAll("\'", "")
|
||
|
||
//parameterName, parameterValue, revocationUrl
|
||
data.components.securitySchemes["Oauth2"] = {
|
||
"type": "oauth2",
|
||
"description": "Oauth2.0 authorizationCode authentication",
|
||
"flow": {
|
||
"authorizationCode": {
|
||
"authorizationUrl": newparamName,
|
||
"tokenUrl": parameterLocation,
|
||
"refreshUrl": refreshUrl,
|
||
"scopes": oauth2Scopes === undefined || oauth2Scopes === null ? [] : oauth2Scopes,
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
if (setExtraAuth.length > 0) {
|
||
for (var key in extraAuth) {
|
||
const curauth = extraAuth[key]
|
||
|
||
if (curauth.name.toLowerCase() == "url") {
|
||
alert.error("Can't add extra auth with Name URL")
|
||
setAppBuilding(false)
|
||
return
|
||
}
|
||
|
||
data.components.securitySchemes[curauth.name] = {
|
||
"type": "apiKey",
|
||
"in": curauth.type,
|
||
"name": curauth.name,
|
||
}
|
||
}
|
||
}
|
||
|
||
fetch(globalUrl+"/api/v1/verify_openapi", {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(data, null, 4),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
//if (response.status !== 200) {
|
||
// setErrorCode("An error occurred during validation")
|
||
// throw new Error("NOT 200 :O")
|
||
//}
|
||
|
||
setAppBuilding(false)
|
||
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 => {
|
||
setAppBuilding(false)
|
||
setErrorCode(error.toString())
|
||
alert.error(error.toString())
|
||
});
|
||
}
|
||
|
||
const bearerAuth = authenticationOption === "Bearer auth" ?
|
||
<div style={{color: "white"}}>
|
||
<h4>
|
||
<a target="_blank" href="https://swagger.io/docs/specification/authentication/bearer-authentication/" style={{textDecoriation: "none", color: "#f85a3e"}}>
|
||
Bearer auth
|
||
</a>
|
||
</h4>
|
||
Users will be required to submit their API as the header "Authorization: Bearer APIKEY"
|
||
</div>
|
||
: null
|
||
|
||
// Basicauth
|
||
const basicAuth = authenticationOption === "Basic auth" ?
|
||
<div style={{color: "white"}}>
|
||
<h4>
|
||
<a target="_blank" href="https://swagger.io/docs/specification/authentication/basic-authentication/" style={{textDecoriation: "none", color: "#f85a3e"}}>
|
||
Basic authentication
|
||
</a>
|
||
</h4>
|
||
Users will be required to submit a valid username and password before using the API
|
||
</div>
|
||
: null
|
||
|
||
// API key
|
||
//const verifyBaseUrl = () => {
|
||
// if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) {
|
||
// return true
|
||
// }
|
||
|
||
// if (baseUrl.endsWith("/")) {
|
||
// return true
|
||
// }
|
||
//
|
||
// return false
|
||
//}
|
||
|
||
//const verifyApiParameter = () => {
|
||
// const notAllowed = ["!","#","$","%","&","'","^","+","-",".","_","~","|","]","+","$",]
|
||
// for (var key in notAllowed) {
|
||
// if (parameterName.includes(notAllowed[key])) {
|
||
// return false
|
||
// }
|
||
// }
|
||
|
||
// return true
|
||
//}
|
||
|
||
const testAction = (index) => {
|
||
console.log("Should test action at index "+index)
|
||
console.log(actions[index])
|
||
}
|
||
|
||
const addPathQuery = () => {
|
||
urlPathQueries.push({"name": "", "required": true})
|
||
if (updater === "addupdater") {
|
||
setUpdater("updater")
|
||
} else {
|
||
setUpdater("addupdater")
|
||
}
|
||
setUrlPathQueries(urlPathQueries)
|
||
}
|
||
|
||
const flipRequired = (index) => {
|
||
urlPathQueries[index].required = !urlPathQueries[index].required
|
||
if (updater === "flipupdater") {
|
||
setUpdater("updater")
|
||
} else {
|
||
setUpdater("flipupdater")
|
||
}
|
||
setUrlPathQueries(urlPathQueries)
|
||
|
||
}
|
||
|
||
const deletePathQuery = (index) => {
|
||
urlPathQueries.splice(index, 1)
|
||
if (updater === "deleteupdater") {
|
||
setUpdater("updater")
|
||
} else {
|
||
setUpdater("deleteupdater")
|
||
}
|
||
setUrlPathQueries(urlPathQueries)
|
||
}
|
||
|
||
const duplicateAction = (index) => {
|
||
var newAction = JSON.parse(JSON.stringify(actions[index]))
|
||
newAction.name = newAction.name+"_copy"
|
||
newAction.errors.push("Can't have the same name")
|
||
|
||
actions.push(newAction)
|
||
|
||
if (actions.length > actionAmount) {
|
||
setActionAmount(actions.length)
|
||
}
|
||
|
||
setActions(actions)
|
||
setFilteredActions(actions)
|
||
setUpdate(Math.random())
|
||
}
|
||
|
||
const deleteAction = (index) => {
|
||
actions.splice(index, 1)
|
||
setCurrentAction({
|
||
"name": "",
|
||
"description": "",
|
||
"url": "",
|
||
"file_field": "",
|
||
"headers": "",
|
||
"paths": [],
|
||
"queries": [],
|
||
"body": "",
|
||
"errors": [],
|
||
"method": actionNonBodyRequest[0],
|
||
})
|
||
|
||
setActions(actions)
|
||
setFilteredActions(actions)
|
||
setActionAmount(actionAmount-1)
|
||
setUpdate(Math.random())
|
||
}
|
||
|
||
//console.log("Option: ", authenticationOption)
|
||
//console.log("Location: ", parameterLocation)
|
||
//console.log("Name: ", parameterName)
|
||
const extraKeys =
|
||
<div style={{marginTop: 50}}>
|
||
<div style={{display: "flex"}}>
|
||
<Typography variant="body1">Extra configuration items</Typography>
|
||
{extraAuth.length === 0 ?
|
||
<Button color="primary" style={{maxWidth: 50, marginLeft: 15, }} variant="contained" onClick={() => {
|
||
console.log("ADD NEW!")
|
||
extraAuth.push(defaultAuth)
|
||
setExtraAuth(extraAuth)
|
||
setUpdate(Math.random())
|
||
}}>
|
||
<AddIcon style={{}}/>
|
||
</Button>
|
||
: <span style={{width: 50, }}/>}
|
||
</div>
|
||
{extraAuth.map((value, index) => {
|
||
return (
|
||
<span key={index} style={{display: "flex", height: 50, marginTop: 5, }}>
|
||
<TextField
|
||
required
|
||
style={{height: 50, flex: 2, marginTop: 0, marginBottom: 0, backgroundColor: inputColor, marginRight: 5, }}
|
||
fullWidth={true}
|
||
placeholder="Name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={extraAuth[index].name}
|
||
onChange={e => {
|
||
extraAuth[index].name = e.target.value
|
||
setExtraAuth(extraAuth)
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: 5,
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
/>
|
||
<TextField
|
||
required
|
||
style={{height: 50, marginTop: 0, marginBottom: 0, flex: 2, backgroundColor: inputColor, marginRight: 5,}}
|
||
fullWidth={true}
|
||
placeholder="Example - input an example for the user"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={extraAuth[index].example}
|
||
onChange={e => {
|
||
extraAuth[index].example = e.target.value
|
||
setExtraAuth(extraAuth)
|
||
}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
minHeight: 50,
|
||
marginLeft: 5,
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
/>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
extraAuth[index].type = e.target.value
|
||
setUpdate(Math.random())
|
||
setExtraAuth(extraAuth)
|
||
}}
|
||
value={extraAuth[index].type}
|
||
style={{flex: 1, backgroundColor: inputColor, paddingLeft: "10px", color: "white", height: 50, borderRadius: 5, }}
|
||
inputProps={{
|
||
name: 'age',
|
||
id: 'outlined-age-simple',
|
||
}}
|
||
>
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={"header"}>
|
||
Header
|
||
</MenuItem>
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={"query"}>
|
||
Query
|
||
</MenuItem>
|
||
</Select>
|
||
<div style={{display: "flex", width: 100, }}>
|
||
{index === extraAuth.length-1 ?
|
||
<Button color="primary" style={{}} variant="outlined" onClick={() => {
|
||
extraAuth.push(defaultAuth)
|
||
setExtraAuth(extraAuth)
|
||
setUpdate(Math.random())
|
||
}}>
|
||
<AddIcon style={{}}/>
|
||
</Button>
|
||
: <span style={{}}/>}
|
||
<Button color="primary" style={{}} variant="outlined" onClick={() => {
|
||
const tmpAuth = extraAuth.filter(item => item.type === value.type && item.name !== value.name)
|
||
setExtraAuth(tmpAuth)
|
||
console.log(tmpAuth)
|
||
setUpdate(Math.random())
|
||
}}>
|
||
<RemoveIcon style={{}}/>
|
||
</Button>
|
||
</div>
|
||
</span>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
const jwtAuth = authenticationOption === "JWT" ?
|
||
<div style={{color: "white", marginTop: 20, }}>
|
||
<Typography variant="body1">JWT authentication</Typography>
|
||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10,}}>
|
||
Authentication path
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{margin: 0, flex: "1", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="/security/user/authenticate"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={parameterName}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Must start with / and be a valid path</span>}
|
||
onBlur={e => setParameterName(e.target.value)}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
:
|
||
null
|
||
|
||
const oauth2Auth = authenticationOption === "Oauth2" ?
|
||
<div style={{color: "white", marginTop: 20, }}>
|
||
<Typography variant="body1">Oauth2 authentication</Typography>
|
||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10,}}>
|
||
Base Authorization URL
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{margin: 0, flex: "1", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="https://.../oauth2/authorize"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={parameterName}
|
||
onChange={e => setParameterName(e.target.value)}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10,}}>
|
||
Token URL
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{margin: 0, flex: "1", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="https://.../oauth2/token"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={parameterLocation}
|
||
onChange={e => setParameterLocation(e.target.value)}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10,}}>
|
||
Refresh-token URL
|
||
</Typography>
|
||
<TextField
|
||
style={{margin: 0, flex: "1", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="The URL to retrieve refresh-tokens at"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={refreshUrl}
|
||
onChange={e => setRefreshUrl(e.target.value)}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10,}}>
|
||
Scopes
|
||
</Typography>
|
||
<ChipInput
|
||
style={{}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
maxHeight: 50,
|
||
},
|
||
}}
|
||
style={{maxHeight: 80, overflowX: "hidden", overflowY: "auto",}}
|
||
placeholder="Scopes"
|
||
color="primary"
|
||
fullWidth
|
||
value={oauth2Scopes}
|
||
onAdd={(chip) => {
|
||
oauth2Scopes.push(chip)
|
||
console.log(oauth2Scopes)
|
||
setOauth2Scopes(oauth2Scopes)
|
||
setUpdate(Math.random())
|
||
}}
|
||
onDelete={(chip, index) => {
|
||
oauth2Scopes.splice(index, 1)
|
||
console.log(oauth2Scopes)
|
||
setOauth2Scopes(oauth2Scopes)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
</div>
|
||
: null
|
||
|
||
const apiKey = authenticationOption === "API key" ?
|
||
<div style={{color: "white", marginTop: 20, }}>
|
||
<Typography variant="body1">API key authentication</Typography>
|
||
<Typography variant="body2" color="textSecondary">
|
||
Add the name of the field used for authentication, e.g. "X-APIKEY"
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{flex: "1", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="Field Name (not token)"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={parameterName}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Can't be empty. Can't contain any of the following characters: !#$%&'^"+-._~|]+$</span>}
|
||
onChange={e => {
|
||
setParameterName(e.target.value)
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
Field type
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setParameterLocation(e.target.value)
|
||
}}
|
||
value={parameterLocation}
|
||
style={{borderRadius: 5, backgroundColor: inputColor, paddingLeft: "10px", color: "white", height: "50px"}}
|
||
inputProps={{
|
||
name: 'age',
|
||
id: 'outlined-age-simple',
|
||
}}
|
||
>
|
||
{apikeySelection.map((data, index) => {
|
||
if (data === undefined) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
)}
|
||
)}
|
||
</Select>
|
||
</div>
|
||
: null
|
||
|
||
const loopQueries = urlPathQueries.length === 0 ?
|
||
null :
|
||
<div>
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
Queries
|
||
{urlPathQueries.map((data, index) => {
|
||
const requiredColor = data.required === true ? "green" : "red"
|
||
//const required = data.required === true ? <div style={{color: "green", cursor: "pointer"}}>{data.required.toString()}</div> : <div onClick={() => {flipRequired(index)}} style={{display: "inline", color: "red", cursor: "pointer"}}>{data.required.toString()}</div>
|
||
return (
|
||
<Paper key={index} style={actionListStyle}>
|
||
<div style={{marginLeft: "5px", width: "100%"}}>
|
||
<div style={{cursor: "pointer"}} onClick={() => {flipRequired(index)}}>
|
||
Required: <div style={{display: "inline", color: requiredColor}}>{data.required.toString()}</div>
|
||
</div>
|
||
<TextField
|
||
required
|
||
fullWidth={true}
|
||
defaultValue={data.name}
|
||
placeholder={'Query name'}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Click required switch</span>}
|
||
onBlur={(e) => {
|
||
console.log("IN BLUR: ", e.target.value)
|
||
urlPathQueries[index].name = e.target.value.replaceAll("=", "")
|
||
|
||
setUrlPathQueries(urlPathQueries)
|
||
}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
|
||
</div>
|
||
<div style={{float: "right", color: "#f85a3e", cursor: "pointer"}} onClick={() => {deletePathQuery(index)}}>
|
||
Delete
|
||
</div>
|
||
|
||
</Paper>
|
||
)
|
||
})}
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
</div>
|
||
|
||
const loopActions = actions.length === 0 ?
|
||
null
|
||
:
|
||
<div>
|
||
{filteredActions.slice(0,actionAmount).map((data, index) => {
|
||
var error = data.errors.length > 0 ?
|
||
<Tooltip color="primary" title={data.errors.join("\n")} placement="bottom">
|
||
<ErrorOutlineIcon />
|
||
</Tooltip>
|
||
:
|
||
<Tooltip color="secondary" title={data.errors.join("\n")} placement="bottom">
|
||
<CheckCircleIcon style={{marginTop: 6}}/>
|
||
</Tooltip>
|
||
|
||
|
||
var bgColor = "#61afee"
|
||
if (data.method === "POST") {
|
||
bgColor = "#49cc90"
|
||
} else if (data.method === "PUT") {
|
||
bgColor = "#fca130"
|
||
} else if (data.method === "PATCH") {
|
||
bgColor = "#50e3c2"
|
||
} else if (data.method === "DELETE") {
|
||
bgColor = "#f93e3e"
|
||
} else if (data.method === "HEAD") {
|
||
bgColor = "#9012fe"
|
||
}
|
||
|
||
const url = data.url
|
||
const hasFile = data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0
|
||
return (
|
||
<Paper key={index} style={actionListStyle}>
|
||
{error}
|
||
<Tooltip title="Edit action" placement="bottom">
|
||
<div style={{marginLeft: "5px", width: "100%", cursor: "pointer", maxWidth: 725, overflowX: "hidden",}} onClick={() => {
|
||
setCurrentAction(data)
|
||
setCurrentActionMethod(data.method)
|
||
setUrlPathQueries(data.queries)
|
||
setUrlPath(data.url)
|
||
setActionsModalOpen(true)
|
||
|
||
if (data["body"] !== undefined && data["body"] !== null && data["body"].length > 0) {
|
||
findBodyParams(data["body"])
|
||
}
|
||
|
||
if (hasFile) {
|
||
setFileUploadEnabled(true)
|
||
}
|
||
}}>
|
||
<div style={{display: "flex"}}>
|
||
<Chip
|
||
style={{backgroundColor: bgColor, color: "white", borderRadius: 5, minWidth: 80, marginRight: 10, marginTop: 2, cursor: "pointer", fontSize: 14,}}
|
||
label={data.method}
|
||
/>
|
||
<span style={{fontSize: 16, marginTop: "auto", marginBottom: "auto",}}>
|
||
{hasFile ? <AttachFileIcon style={{height: 20, width: 20}} /> : null} {url} - {data.name}
|
||
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Tooltip>
|
||
{/*
|
||
<Tooltip title="Test action" placement="bottom">
|
||
<div style={{color: "#f85a3e", cursor: "pointer", marginRight: "10px", }} onClick={() => {testAction(index)}}>
|
||
Test
|
||
</div>
|
||
</Tooltip>
|
||
*/}
|
||
<Tooltip title="Duplicate action" placement="bottom" style={{minWidth: 60}} >
|
||
<div style={{color: "#f85a3e", cursor: "pointer", marginRight: 15, }} onClick={() => {
|
||
duplicateAction(index)
|
||
}}>
|
||
<FileCopyIcon color="secondary"/>
|
||
</div>
|
||
</Tooltip>
|
||
<Tooltip title="Delete action" placement="bottom" style={{minWidth: 60}} >
|
||
<div style={{color: "#f85a3e", cursor: "pointer"}} onClick={() => {deleteAction(index)}}>
|
||
<DeleteIcon color="secondary"/>
|
||
</div>
|
||
</Tooltip>
|
||
</Paper>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
const setActionField = (field, value) => {
|
||
currentAction[field] = value
|
||
setCurrentAction(currentAction)
|
||
|
||
//setUrlPathQueries(currentAction.queries)
|
||
}
|
||
|
||
const findBodyParams = (body) => {
|
||
const regex = /\${(\w+)}/g
|
||
const found = body.match(regex)
|
||
if (found === null) {
|
||
setExtraBodyFields([])
|
||
} else {
|
||
setExtraBodyFields(found)
|
||
}
|
||
}
|
||
|
||
const HandleIndividualChip = (props) => {
|
||
const { chipData, index } = props;
|
||
const [chipRequired, setChipRequired] = useState(false)
|
||
|
||
return (
|
||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||
<Chip
|
||
style={{
|
||
backgroundColor: chipRequired ? "#f86a3e" : "#3d3f43",
|
||
height: 30,
|
||
margin: 3,
|
||
paddingLeft: 5,
|
||
paddingRight: 5,
|
||
height: 28,
|
||
cursor: "pointer",
|
||
borderColor: "#3d3f43",
|
||
color: "white",
|
||
}}
|
||
label={chipData}
|
||
onClick={() => {
|
||
console.log("CLICK: ", chipData)
|
||
setChipRequired(!chipRequired)
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
)
|
||
}
|
||
|
||
const SetExtraBodyField = (props) => {
|
||
const { extraBodyFields } = props;
|
||
|
||
if (extraBodyFields === undefined || extraBodyFields === null) {
|
||
return null
|
||
}
|
||
|
||
//const parsedlist = extraBodyFields.join(", ")
|
||
//console.log("LIST: ", parsedlist)
|
||
|
||
return (
|
||
<span>
|
||
{extraBodyFields.map((data, index) => {
|
||
return (
|
||
<HandleIndividualChip key={index} chipData={data} />
|
||
)
|
||
})}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
const bodyInfo = actionBodyRequest.includes(currentActionMethod) ?
|
||
<div style={{marginTop: 10}}>
|
||
<b>Request Body</b>: {extraBodyFields.length > 0 ?
|
||
<Typography style={{display: "inline-block"}}>
|
||
<SetExtraBodyField extraBodyFields={extraBodyFields}/>
|
||
</Typography>
|
||
:
|
||
<Typography style={{display: "inline-block"}}>
|
||
{`Add variables with \$\{ variable_name }`}
|
||
</Typography>
|
||
}
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginRight: "15px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder={'{\n\t"username": "${username}",\n\t"apikey": "${apikey}",\n\t"search": "1.2.3.5"}'}
|
||
margin="normal"
|
||
variant="outlined"
|
||
multiline
|
||
rows="5"
|
||
defaultValue={currentAction["body"]}
|
||
onChange={e => {
|
||
setActionField("body", e.target.value)
|
||
findBodyParams(e.target.value)
|
||
}}
|
||
key={currentAction}
|
||
helperText={
|
||
<span style={{color:"white", marginBottom: "2px",}}>
|
||
Shows an example body to the user. ${} creates variables.
|
||
</span>
|
||
}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<div>
|
||
</div>
|
||
</div>
|
||
: null
|
||
|
||
const exampleResponse =
|
||
<div style={{}}>
|
||
<b>Example success response</b>
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginRight: "15px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder={'{\n\t"email": "testing@test.com",\n\t"firstname": "testing"\n}'}
|
||
margin="normal"
|
||
variant="outlined"
|
||
multiline
|
||
rows="2"
|
||
defaultValue={currentAction["example_response"]}
|
||
onChange={e => setActionField("example_response", e.target.value)}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>
|
||
Helps with autocompletion and understanding of the endpoint
|
||
</span>}
|
||
key={currentAction}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
const addActionToView = (errors) => {
|
||
currentAction.errors = errors
|
||
currentAction.queries = urlPathQueries
|
||
setUrlPathQueries([])
|
||
|
||
const actionIndex = actions.findIndex(data => data.name === currentAction.name)
|
||
if (actionIndex < 0) {
|
||
actions.push(currentAction)
|
||
} else {
|
||
actions[actionIndex] = currentAction
|
||
}
|
||
|
||
|
||
if (actions.length > actionAmount) {
|
||
setActionAmount(actions.length)
|
||
}
|
||
|
||
setActions(actions)
|
||
setFilteredActions(actions)
|
||
}
|
||
|
||
const getActionErrors = () => {
|
||
var errormessage = []
|
||
if (currentAction.name === undefined || currentAction.name.length === 0) {
|
||
errormessage.push("Name can't be empty")
|
||
}
|
||
|
||
// Url verification
|
||
//if (currentAction.url.length === 0) {
|
||
// errormessage.push("URL path can't be empty.")
|
||
if (!currentAction.url.startsWith("/") && baseUrl.length > 0 && currentAction.url.length > 0) {
|
||
errormessage.push("URL must start with /")
|
||
}
|
||
|
||
const check = urlPathQueries.findIndex(data => data.name.length === 0)
|
||
if (check >= 0) {
|
||
errormessage.push("All queries must have a value")
|
||
}
|
||
|
||
return errormessage
|
||
}
|
||
|
||
const getCurrentPaths = (urlPath) => {
|
||
var paths = []
|
||
var queries = []
|
||
|
||
if (urlPath.includes("{") && urlPath.includes("}")) {
|
||
var tmpWord = ""
|
||
var record = false
|
||
|
||
var query = false
|
||
for (var key in urlPath) {
|
||
if (urlPath[key] === "?") {
|
||
query = true
|
||
}
|
||
|
||
if (urlPath[key] === "}") {
|
||
if (tmpWord === parameterName) {
|
||
tmpWord = ""
|
||
record = false
|
||
continue
|
||
} else if (query) {
|
||
queries.push(tmpWord)
|
||
} else {
|
||
paths.push(tmpWord)
|
||
}
|
||
|
||
tmpWord = ""
|
||
record = false
|
||
}
|
||
|
||
if (record) {
|
||
tmpWord += urlPath[key]
|
||
}
|
||
|
||
//if (urlPath[key] === "{" && urlPath[key-1] === "/") {
|
||
if (urlPath[key] === "{") {
|
||
record = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if (urlPath.includes("<") && urlPath.includes(">")) {
|
||
var tmpWord = ""
|
||
var record = false
|
||
|
||
var query = false
|
||
for (var key in urlPath) {
|
||
if (urlPath[key] === "?") {
|
||
query = true
|
||
}
|
||
|
||
if (urlPath[key] === ">") {
|
||
if (tmpWord === parameterName) {
|
||
tmpWord = ""
|
||
record = false
|
||
continue
|
||
} else if (query) {
|
||
queries.push(tmpWord)
|
||
} else {
|
||
paths.push(tmpWord)
|
||
}
|
||
|
||
tmpWord = ""
|
||
record = false
|
||
}
|
||
|
||
if (record) {
|
||
tmpWord += urlPath[key]
|
||
}
|
||
|
||
//if (urlPath[key] === "{" && urlPath[key-1] === "/") {
|
||
if (urlPath[key] === "<") {
|
||
record = true
|
||
}
|
||
}
|
||
}
|
||
|
||
return [paths, queries]
|
||
}
|
||
|
||
const UrlPathParameters = () => {
|
||
const values = getCurrentPaths(urlPath)
|
||
const paths = values[0]
|
||
const queries = values[1]
|
||
|
||
if (currentAction.paths !== paths && urlPath.length > 0) {
|
||
//console.log("IN PATHS SETTER: !", paths)
|
||
setActionField("paths", paths)
|
||
}
|
||
|
||
var tmpQueries = []
|
||
|
||
// No overlapping of names
|
||
for (var key in queries) {
|
||
const tmpquery = queries[key]
|
||
const found = tmpQueries.find(query => query.name === tmpquery)
|
||
if (found === undefined) {
|
||
tmpQueries.push({"name": queries[key], required: true})
|
||
}
|
||
}
|
||
|
||
// FIXME: Frontend isn't updating..
|
||
if (tmpQueries.length > 0 && JSON.stringify(tmpQueries) !== JSON.stringify(urlPathQueries)) {
|
||
setUrlPathQueries(tmpQueries)
|
||
}
|
||
|
||
return paths.length > 0 ?
|
||
<div>
|
||
Required parameters: {paths.join(", ")}
|
||
</div>
|
||
: null
|
||
}
|
||
|
||
const newActionModal =
|
||
<Dialog
|
||
open={actionsModalOpen}
|
||
fullWidth
|
||
onClose={() => {
|
||
setUrlPath("")
|
||
setCurrentAction({
|
||
"name": "",
|
||
"description": "",
|
||
"url": "",
|
||
"file_field": "",
|
||
"headers": "",
|
||
"paths": [],
|
||
"queries": [],
|
||
"body": "",
|
||
"errors": [],
|
||
"method": actionNonBodyRequest[0],
|
||
})
|
||
setCurrentActionMethod(apikeySelection[0])
|
||
setUrlPathQueries([])
|
||
setActionsModalOpen(false)
|
||
setFileUploadEnabled(false)
|
||
}}
|
||
>
|
||
<FormControl style={{backgroundColor: surfaceColor, color: "white",}}>
|
||
<DialogTitle><div style={{color: "white"}}>New action</div></DialogTitle>
|
||
<DialogContent>
|
||
<a target="_blank" href="https://shuffler.io/docs/app_creation#actions" style={{textDecoration: "none", color: "#f85a3e"}}>Learn more about actions</a>
|
||
<div style={{marginTop: "15px"}}/>
|
||
Name
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginTop: 5, marginRight: 15, backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="Name"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={currentAction["name"]}
|
||
onChange={e => {
|
||
setActionField("name", e.target.value)
|
||
}}
|
||
onBlur={e => {
|
||
// Fix basic issues in frontend. Python functions run a-zA-Z0-9_
|
||
const regex = /[A-Za-z0-9 _]/g;
|
||
const found = e.target.value.match(regex);
|
||
if (found !== null) {
|
||
setActionField("name", found.join(""))
|
||
}
|
||
}}
|
||
key={currentAction}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
<div style={{marginTop: 10}}/>
|
||
Description
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginTop: "5px", marginRight: "15px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="Description"
|
||
type="description"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={currentAction["description"]}
|
||
onChange={e => setActionField("description", e.target.value)}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
<h2>Request</h2>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setActionField("method", e.target.value)
|
||
setCurrentActionMethod(e.target.value)
|
||
}}
|
||
value={currentActionMethod}
|
||
style={{backgroundColor: inputColor, paddingLeft: "10px", color: "white", height: "50px"}}
|
||
inputProps={{
|
||
name: 'Method',
|
||
id: 'method-option',
|
||
}}
|
||
>
|
||
{actionNonBodyRequest.map((data, index) => {
|
||
return (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
{actionBodyRequest.map((data, index) => (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
<div style={{marginTop: "15px"}} />
|
||
URL path / Curl statement
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginRight: "15px", marginTop: "5px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="URL path"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={urlPath}
|
||
onChange={e => {
|
||
setActionField("url", e.target.value)
|
||
setUrlPath(e.target.value)
|
||
console.log(e.target.value)
|
||
}}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>The path to use. Must start with /. Use {"{variablename}"} to have path variables</span>}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
input: classes.input,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
onBlur={event => {
|
||
var parsedurl = event.target.value
|
||
//console.log("URL: ", parsedurl)
|
||
if (parsedurl.includes(" ")) {
|
||
parsedurl = parsedurl.replaceAll(" ", " ")
|
||
}
|
||
|
||
if (parsedurl.includes(" ")) {
|
||
parsedurl = parsedurl.replaceAll(" ", " ")
|
||
}
|
||
|
||
if (parsedurl.includes("[") && parsedurl.includes("]")) {
|
||
//console.log("REPLACE1")
|
||
parsedurl = parsedurl.replaceAll("[", "{")
|
||
parsedurl = parsedurl.replaceAll("]", "}")
|
||
}
|
||
|
||
if (parsedurl.includes("<") && parsedurl.includes(">")) {
|
||
//console.log("REPLACE2")
|
||
parsedurl = parsedurl.replaceAll("<", "{")
|
||
parsedurl = parsedurl.replaceAll(">", "}")
|
||
}
|
||
|
||
//console.log("URL2: ", parsedurl)
|
||
if (parsedurl.startsWith("PUT ") || parsedurl.startsWith("GET ") ||parsedurl.startsWith("POST ") || parsedurl.startsWith("DELETE ") ||parsedurl.startsWith("PATCH ") || parsedurl.startsWith("CONNECT ")) {
|
||
|
||
const tmp = parsedurl.split(" ")
|
||
|
||
if (tmp.length > 1) {
|
||
parsedurl = tmp[1].trim()
|
||
setActionField("url", parsedurl)
|
||
|
||
setCurrentActionMethod(tmp[0].toUpperCase())
|
||
setActionField("method", tmp[0].toUpperCase())
|
||
}
|
||
|
||
console.log("URL3: ", parsedurl)
|
||
|
||
setUpdate(Math.random())
|
||
} else if (parsedurl.startsWith("curl")) {
|
||
console.log("URL4: ", parsedurl)
|
||
|
||
const request = parseCurl(event.target.value)
|
||
if (request !== event.target.value && request.method !== undefined && request.method !== null) {
|
||
if (request.method.toUpperCase() !== currentAction.Method) {
|
||
setCurrentActionMethod(request.method.toUpperCase())
|
||
setActionField("method", request.method.toUpperCase())
|
||
}
|
||
|
||
if (request.header !== undefined && request.header !== null) {
|
||
var headers = []
|
||
for (let [key, value] of Object.entries(request.header)) {
|
||
if (parameterName !== undefined && key.toLowerCase() === parameterName.toLowerCase()) {
|
||
continue
|
||
}
|
||
|
||
if (key === "Authorization" && authenticationOption === "Bearer auth") {
|
||
continue
|
||
}
|
||
|
||
headers += key+"="+value+"\n"
|
||
}
|
||
|
||
setActionField("headers", headers)
|
||
}
|
||
|
||
if (request.body !== undefined && request.body !== null) {
|
||
setActionField("body", request.body)
|
||
}
|
||
|
||
// Parse URL
|
||
if (request.url !== undefined) {
|
||
parsedurl = request.url
|
||
}
|
||
}
|
||
|
||
console.log("PARSED: ", parsedurl)
|
||
if (parsedurl !== undefined) {
|
||
if (parsedurl.includes("<") && parsedurl.includes(">")) {
|
||
parsedurl = parsedurl.split("<").join("{")
|
||
parsedurl = parsedurl.split(">").join("}")
|
||
}
|
||
|
||
if (parsedurl.startsWith("http") || parsedurl.startsWith("ftp")) {
|
||
if (parsedurl !== undefined && parsedurl.includes(parameterName)) {
|
||
// Remove <> etc.
|
||
//
|
||
|
||
console.log("IT HAS THE PARAM NAME!")
|
||
const newurl = new URL(encodeURI(parsedurl))
|
||
newurl.searchParams.delete(parameterName)
|
||
parsedurl = decodeURI(newurl.href)
|
||
}
|
||
|
||
// Remove the base URL itself
|
||
if (parsedurl !== undefined && baseUrl !== undefined && baseUrl.length > 0 && parsedurl.includes(baseUrl)) {
|
||
parsedurl = parsedurl.replace(baseUrl, "")
|
||
}
|
||
|
||
// Check URL query && headers
|
||
//setActionField("url", parsedurl)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (event.target.value !== parsedurl) {
|
||
setUrlPath(parsedurl)
|
||
setActionField("url", parsedurl)
|
||
}
|
||
//console.log("URL: ", request.url)
|
||
}}
|
||
/>
|
||
<UrlPathParameters />
|
||
{loopQueries}
|
||
<Button color="primary" style={{marginTop: "5px", marginBottom: "10px", borderRadius: "0px"}} variant="outlined" onClick={() => {
|
||
addPathQuery()
|
||
}}>New query</Button>
|
||
{currentActionMethod === "POST" ?
|
||
<Button color="primary" variant={fileUploadEnabled ? "contained" : "outlined"} style={{marginLeft: 10, marginTop: "5px", marginBottom: "10px", borderRadius: "0px"}} onClick={() => {
|
||
setFileUploadEnabled(!fileUploadEnabled)
|
||
if (fileUploadEnabled && currentAction["file_field"].length > 0) {
|
||
setActionField("file_field", "")
|
||
}
|
||
setUpdate(Math.random())
|
||
}}>Enable Fileupload</Button>
|
||
: null}
|
||
{fileUploadEnabled ?
|
||
<TextField
|
||
required
|
||
style={{backgroundColor: inputColor, display: "inline-block", marginLeft: 10, maxWidth: 210, marginTop: 7, }}
|
||
placeholder={"file"}
|
||
margin="normal"
|
||
variant="outlined"
|
||
id="standard-required"
|
||
defaultValue={currentAction["file_field"]}
|
||
onChange={e => setActionField("file_field", e.target.value)}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>The File field to interact with</span>}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
: null}
|
||
<div/>
|
||
<b>Headers</b>: static for the action
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginRight: "15px", marginTop: "5px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder={"Accept: application/json\r\nContent-Type: application/json"}
|
||
margin="normal"
|
||
variant="outlined"
|
||
id="standard-required"
|
||
defaultValue={currentAction["headers"]}
|
||
multiline
|
||
rows="2"
|
||
onChange={e => setActionField("headers", e.target.value)}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Headers that are part of the request. Default: EMPTY</span>}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
{bodyInfo}
|
||
<Divider style={{backgroundColor: "rgba(255,255,255,0.5)", marginTop: 15, marginBottom: 15}} />
|
||
{exampleResponse}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button style={{borderRadius: "0px"}} onClick={() => {
|
||
setActionsModalOpen(false)}}>
|
||
Cancel
|
||
</Button>
|
||
<Button color="primary" variant="outlined" style={{borderRadius: "0px"}} onClick={() => {
|
||
//console.log(urlPathQueries)
|
||
//console.log(urlPath)
|
||
console.log(currentAction)
|
||
const errors = getActionErrors()
|
||
addActionToView(errors)
|
||
setActionsModalOpen(false)
|
||
setUrlPathQueries([])
|
||
setUrlPath("")
|
||
setFileUploadEnabled(false)
|
||
|
||
}}>
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</FormControl>
|
||
</Dialog>
|
||
|
||
|
||
const categories = [
|
||
"Communication",
|
||
"Cases",
|
||
"SIEM",
|
||
"Assets",
|
||
"Intel",
|
||
"IAM",
|
||
"Network",
|
||
"Eradication",
|
||
"Other",
|
||
]
|
||
|
||
const tagView =
|
||
<div style={{color: "white"}}>
|
||
{/*
|
||
<ChipInput
|
||
style={{marginTop: 10}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
placeholder="Categories"
|
||
color="primary"
|
||
fullWidth
|
||
value={newWorkflowCategories}
|
||
onAdd={(chip) => {
|
||
newWorkflowCategories.push(chip)
|
||
setNewWorkflowCategories(newWorkflowCategories)
|
||
setUpdate("added "+chip)
|
||
}}
|
||
onDelete={(chip, index) => {
|
||
newWorkflowCategories.splice(index, 1)
|
||
setNewWorkflowCategories(newWorkflowCategories)
|
||
setUpdate("delete "+chip)
|
||
}}
|
||
/>
|
||
*/}
|
||
<h4>Categories</h4>
|
||
<Select
|
||
fullWidth
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
}
|
||
}}
|
||
onChange={(e) => {
|
||
setNewWorkflowCategories([e.target.value])
|
||
setUpdate("added "+e.target.value)
|
||
}}
|
||
value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{categories.map((data, index) => (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
<h4>Tags</h4>
|
||
<ChipInput
|
||
style={{marginTop: 10}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
placeholder="Tags"
|
||
color="primary"
|
||
fullWidth
|
||
value={newWorkflowTags}
|
||
onAdd={(chip) => {
|
||
newWorkflowTags.push(chip)
|
||
setNewWorkflowTags(newWorkflowTags)
|
||
setUpdate("added"+chip)
|
||
}}
|
||
onDelete={(chip, index) => {
|
||
newWorkflowTags.splice(index, 1)
|
||
setNewWorkflowTags(newWorkflowTags)
|
||
setUpdate("delete "+chip)
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
const ParsedActionHandler = () => {
|
||
const passedOrg = {"id": "", "name": ""}
|
||
const owner = ""
|
||
const passedTags = ["single test"]
|
||
|
||
const [, setUpdate] = useState()
|
||
const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false);
|
||
const [selectedApp, setSelectedApp] = useState({
|
||
versions: [{
|
||
"id": selectedAction.app_id,
|
||
"version": selectedAction.app_version,
|
||
}],
|
||
loop_versions: [selectedAction.app_version],
|
||
id: selectedAction.app_id,
|
||
name: selectedAction.app_name,
|
||
version: selectedAction.app_version,
|
||
})
|
||
|
||
const [requiresAuthentication, setRequiresAuthentication] = useState(app.authentication !== null && app.authentication !== undefined && app.authentication.required && app.authentication.parameters !== undefined && app.authentication.parameters !== null ? true : false)
|
||
const [workflow, setWorkflow] = useState({
|
||
name: "",
|
||
description: "",
|
||
actions: [selectedAction],
|
||
start: selectedAction.id,
|
||
tags: passedTags,
|
||
execution_org: passedOrg,
|
||
org_id: passedOrg.id,
|
||
id: uuidv4(),
|
||
isValid: true,
|
||
owner: owner,
|
||
created: Date.now(),
|
||
})
|
||
|
||
const EndpointData = () => {
|
||
const [tmpVar, setTmpVar] = React.useState("")
|
||
|
||
return (
|
||
<div>
|
||
The API endpoint to use (URL) - predefined in the app
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type="text"
|
||
color="primary"
|
||
disabled={true}
|
||
placeholder="Bearer token"
|
||
defaultValue={selectedApp.link}
|
||
onChange={(event) => {
|
||
setTmpVar(event.target.value)
|
||
}}
|
||
onBlur={() => {
|
||
selectedApp.link = tmpVar
|
||
console.log("LINK: ", selectedApp.link)
|
||
setSelectedApp(selectedApp)
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const setAppActionAuthentication = (newauth) => {
|
||
if (app.authentication.required) {
|
||
var findAuthId = ""
|
||
if (selectedAction.authentication_id !== null && selectedAction.authentication_id !== undefined && selectedAction.authentication_id.length > 0) {
|
||
findAuthId = selectedAction.authentication_id
|
||
}
|
||
|
||
var baseAuthOptions = []
|
||
for (var key in newauth) {
|
||
var item = newauth[key]
|
||
|
||
const newfields = {}
|
||
for (var filterkey in item.fields) {
|
||
newfields[item.fields[filterkey].key] = item.fields[filterkey].value
|
||
}
|
||
|
||
item.fields = newfields
|
||
if (item.app.name === app.name) {
|
||
baseAuthOptions.push(item)
|
||
|
||
if (item.id === findAuthId) {
|
||
selectedAction.selectedAuthentication = item
|
||
}
|
||
}
|
||
}
|
||
|
||
selectedAction.authentication = baseAuthOptions
|
||
//console.log("Authentication: ", authenticationOptions)
|
||
if (selectedAction.selectedAuthentication === null || selectedAction.selectedAuthentication === undefined || selectedAction.selectedAuthentication.length === "") {
|
||
selectedAction.selectedAuthentication = {}
|
||
}
|
||
} else {
|
||
selectedAction.authentication = []
|
||
selectedAction.authentication_id = ""
|
||
selectedAction.selectedAuthentication = {}
|
||
}
|
||
|
||
setSelectedAction(selectedAction)
|
||
console.log(selectedAction)
|
||
}
|
||
|
||
//{selectedAction.authentication !== undefined && selectedAction.authentication.length > 0 ?
|
||
const getAppAuthentication = () => {
|
||
fetch(globalUrl+"/api/v1/apps/authentication", {
|
||
method: 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for apps :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.success && responseJson.data !== undefined && responseJson.data !== null && responseJson.data.length !== 0) {
|
||
var newauth = []
|
||
for (var key in responseJson.data) {
|
||
if (responseJson.data[key].defined === false) {
|
||
continue
|
||
}
|
||
|
||
newauth.push(responseJson.data[key])
|
||
}
|
||
|
||
setAppAuthentication(newauth)
|
||
setAppActionAuthentication(newauth)
|
||
} else {
|
||
if (app.authentication.required) {
|
||
const tmpParams = selectedAction.parameters
|
||
selectedAction.parameters = []
|
||
|
||
for (var paramkey in app.authentication.parameters) {
|
||
var item = app.authentication.parameters[paramkey]
|
||
item.configuration = true
|
||
|
||
const found = selectedAction.parameters.find(param => param.name === item.name)
|
||
if (found === null || found === undefined) {
|
||
selectedAction.parameters.push(item)
|
||
}
|
||
}
|
||
|
||
for (var paramkey in tmpParams) {
|
||
var item = tmpParams[paramkey]
|
||
//item.configuration = true
|
||
|
||
const found = selectedAction.parameters.find(param => param.name === item.name)
|
||
if (found === null || found === undefined) {
|
||
selectedAction.parameters.push(item)
|
||
}
|
||
}
|
||
|
||
setSelectedAction(selectedAction)
|
||
}
|
||
|
||
//alert.error("Failed getting authentications")
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error("Auth loading error: "+error.toString())
|
||
})
|
||
}
|
||
|
||
if (!authLoaded && appAuthentication.length === 0 && selectedAction.id !== undefined) {
|
||
setAuthLoaded(true)
|
||
getAppAuthentication()
|
||
} else if (selectedAction.id === undefined && currentAction.name !== undefined && currentAction.name !== null && currentAction.name.length > 0) {
|
||
var methodName = `${currentAction.method}_${currentAction.name}`
|
||
if (currentAction.method.toLowerCase() === "custom" || currentAction.name.toLowerCase().startsWith(currentAction.method.toLowerCase())) {
|
||
methodName = currentAction.name
|
||
}
|
||
|
||
methodName = methodName.toLowerCase().replaceAll(" ", "_")
|
||
if (app.actions !== null && app.actions !== undefined) {
|
||
var newselectedaction = app.actions.find(item => item.name.toLowerCase().replaceAll(" ", "_") === methodName)
|
||
if (newselectedaction !== undefined && newselectedaction !== null) {
|
||
newselectedaction.app_id = app.id
|
||
newselectedaction.app_name = app.name
|
||
newselectedaction.app_version = app.app_version
|
||
newselectedaction.authentication = []
|
||
newselectedaction.authentication_id = ""
|
||
newselectedaction.selectedAuthentication = {}
|
||
setSelectedAction(newselectedaction)
|
||
}
|
||
}
|
||
}
|
||
|
||
const setNewAppAuth = (appAuthData) => {
|
||
//console.log("DAta: ", appAuthData)
|
||
fetch(globalUrl+"/api/v1/apps/authentication", {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(appAuthData),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for setting app auth :O!")
|
||
}
|
||
|
||
return response.json()
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
alert.error("Failed to set app auth: "+responseJson.reason)
|
||
} else {
|
||
getAppAuthentication(true)
|
||
setAuthenticationModalOpen(false)
|
||
|
||
// Needs a refresh with the new authentication..
|
||
//alert.success("Successfully saved new app auth")
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert.error(error.toString())
|
||
})
|
||
}
|
||
|
||
const AuthenticationData = (props) => {
|
||
const selectedApp = props.app
|
||
console.log("APP: ", selectedApp)
|
||
|
||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||
fields: {},
|
||
label: "",
|
||
usage: [{
|
||
workflow_id: workflow.id,
|
||
}],
|
||
id: uuidv4(),
|
||
active: true,
|
||
})
|
||
|
||
if (selectedApp.authentication === undefined) {
|
||
return null
|
||
}
|
||
|
||
if (selectedApp.authentication.parameters === null || selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0) {
|
||
return null
|
||
}
|
||
|
||
authenticationOption.app.actions = []
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (authenticationOption.fields[selectedApp.authentication.parameters[key].name] === undefined) {
|
||
authenticationOption.fields[selectedApp.authentication.parameters[key].name] = ""
|
||
}
|
||
}
|
||
|
||
const handleSubmitCheck = () => {
|
||
console.log("NEW AUTH: ", authenticationOption)
|
||
if (authenticationOption.label.length === 0) {
|
||
authenticationOption.label = `Auth for ${selectedApp.name}`
|
||
//alert.info("Label can't be empty")
|
||
//return
|
||
}
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (authenticationOption.fields[selectedApp.authentication.parameters[key].name].length === 0) {
|
||
alert.info("Field "+selectedApp.authentication.parameters[key].name+" can't be empty")
|
||
return null
|
||
}
|
||
}
|
||
|
||
console.log("Action: ", selectedAction)
|
||
selectedAction.authentication_id = authenticationOption.id
|
||
selectedAction.selectedAuthentication = authenticationOption
|
||
if (selectedAction.authentication === undefined || selectedAction.authentication === null) {
|
||
selectedAction.authentication = [authenticationOption]
|
||
} else {
|
||
selectedAction.authentication.push(authenticationOption)
|
||
}
|
||
|
||
setSelectedAction(selectedAction)
|
||
|
||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption))
|
||
var newFields = []
|
||
for (const key in newAuthOption.fields) {
|
||
const value = newAuthOption.fields[key]
|
||
newFields.push({
|
||
key: key,
|
||
value: value,
|
||
})
|
||
}
|
||
|
||
console.log("FIELDS: ", newFields)
|
||
newAuthOption.fields = newFields
|
||
setNewAppAuth(newAuthOption)
|
||
//appAuthentication.push(newAuthOption)
|
||
//setAppAuthentication(appAuthentication)
|
||
//
|
||
|
||
setUpdate(authenticationOption.id)
|
||
|
||
/*
|
||
{selectedAction.authentication.map(data => (
|
||
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
*/
|
||
|
||
}
|
||
|
||
if (authenticationOption.label === null || authenticationOption.label === undefined) {
|
||
authenticationOption.label = selectedApp.name+" authentication"
|
||
}
|
||
|
||
console.log("PRE RETURN")
|
||
return (
|
||
<div>
|
||
<DialogContent>
|
||
<a target="_blank" rel="norefferer" href="https://shuffler.io/docs/app_creation#authentication" style={{textDecoration: "none", color: "#f85a3e"}}>What is this?</a><div/>
|
||
These are required fields for authenticating with {selectedApp.name}
|
||
<div style={{marginTop: 15}}/>
|
||
<b>Name - what is this used for?</b>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"Auth july 2020"}
|
||
defaultValue={`Auth for ${selectedApp.name}`}
|
||
onChange={(event) => {
|
||
authenticationOption.label = event.target.value
|
||
}}
|
||
/>
|
||
{selectedApp.link.length > 0 ? <div style={{marginTop: 15}}><EndpointData /></div> : null}
|
||
<Divider style={{marginTop: 15, marginBottom: 15, backgroundColor: "rgb(91, 96, 100)"}}/>
|
||
<div style={{}}/>
|
||
{selectedApp.authentication.parameters.map((data, index) => {
|
||
return (
|
||
<div key={index} style={{marginTop: 10}}>
|
||
<LockOpenIcon style={{marginRight: 10}}/>
|
||
<b>{data.name}</b>
|
||
<TextField
|
||
style={{backgroundColor: inputColor, borderRadius: theme.palette.borderRadius,}}
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type={data.example !== undefined && data.example.includes("***") ? "password" : "text"}
|
||
color="primary"
|
||
placeholder={data.example}
|
||
onChange={(event) => {
|
||
authenticationOption.fields[data.name] = event.target.value
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
})}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{borderRadius: "0px"}}
|
||
onClick={() => {
|
||
setAuthenticationModalOpen(false)
|
||
}} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button style={{borderRadius: "0px"}} onClick={() => {
|
||
handleSubmitCheck()
|
||
}} color="primary">
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</div>
|
||
)
|
||
}
|
||
}
|
||
|
||
const actionView =
|
||
<div style={{color: "white", position: "relative",}}>
|
||
<div style={{position: "absolute", right: 0, top: 0,}}>
|
||
{actionAmount > 0 && actionAmount < filteredActions.length ?
|
||
<Button color="primary" style={{float: "right", borderRadius: 0, textAlign: "center"}} variant="outlined" onClick={() => {
|
||
setActionAmount(filteredActions.length)
|
||
/*
|
||
if (actionAmount+increaseAmount > filteredActions.length) {
|
||
setActionAmount(filteredActions.length)
|
||
} else {
|
||
setActionAmount(actionAmount+increaseAmount)
|
||
}
|
||
*/
|
||
}}>
|
||
See all actions
|
||
</Button>
|
||
: null}
|
||
</div>
|
||
<Typography variant="h6">Actions {actionAmount > 0 ? <span>({actionAmount} / {filteredActions.length})</span> : null}</Typography>
|
||
<Typography variant="body2" style={{marginTop: 10, marginBottom: 10}}>
|
||
Actions are the tasks performed by an app - usually single URL paths for REST API's.
|
||
</Typography>
|
||
|
||
{projectCategories !== undefined && projectCategories !== null && projectCategories.length > 1 ?
|
||
<div style={{marginTop: 10, marginBottom: 10}}>
|
||
{projectCategories.map((tag, index) => {
|
||
const newname = tag.charAt(0).toUpperCase() + tag.slice(1)
|
||
return (
|
||
<Chip
|
||
key={index}
|
||
style={{
|
||
backgroundColor: tag === selectedCategory ? "#f86a3e" : "#3d3f43",
|
||
height: 30,
|
||
margin: 3,
|
||
paddingLeft: 5,
|
||
paddingRight: 5,
|
||
height: 28,
|
||
cursor: "pointer",
|
||
borderColor: "#3d3f43",
|
||
color: "white",
|
||
}}
|
||
label={newname}
|
||
onClick={() => {
|
||
if (selectedCategory === tag) {
|
||
setFilteredActions(actions)
|
||
setSelectedCategory("")
|
||
setActionAmount(50)
|
||
return
|
||
}
|
||
|
||
const foundActions = actions.filter(data => data.category === tag)
|
||
setFilteredActions(foundActions)
|
||
setSelectedCategory(tag)
|
||
if (actionAmount > foundActions.length) {
|
||
setActionAmount(foundActions.length)
|
||
}
|
||
|
||
//{filteredActions.slice(0,actionAmount).map((data, index) => {
|
||
|
||
//console.log("Found: ", foundActions)
|
||
//{filteredActions.slice(0,actionAmount).map((data, index) => {
|
||
//{actions.slice(0,actionAmount).map((data, index) => {
|
||
}}
|
||
variant={selectedCategory === tag ? "contained " : "outlined"}
|
||
color={selectedCategory === tag ? "primary" : "secondary"}
|
||
/>
|
||
)
|
||
})}
|
||
</div>
|
||
: null}
|
||
<div>
|
||
{loopActions}
|
||
<div style={{display: "flex"}}>
|
||
<Button color="primary" style={{marginTop: "20px", borderRadius: "0px"}} variant="outlined" onClick={() => {
|
||
setCurrentAction({
|
||
"name": "",
|
||
"description": "",
|
||
"url": "",
|
||
"file_field": "",
|
||
"headers": "",
|
||
"queries": [],
|
||
"paths": [],
|
||
"body": "",
|
||
"errors": [],
|
||
"method": actionNonBodyRequest[0],
|
||
})
|
||
setCurrentActionMethod(actionNonBodyRequest[0])
|
||
setActionsModalOpen(true)
|
||
}}>New action</Button>
|
||
{/*
|
||
{actionAmount} {actions.length}
|
||
*/}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
const testView =
|
||
<div style={{color: "white"}}>
|
||
<h2>Test</h2>
|
||
Test an action to see whether it performs in an expected way.
|
||
<a target="_blank" href="https://shuffler.io/docs/app_creation#testing" style={{textDecoration: "none", color: "#f85a3e"}}> TBD: Click here to learn more about testing</a>.
|
||
<div>
|
||
Test :)
|
||
</div>
|
||
</div>
|
||
|
||
var image = ""
|
||
const editHeaderImage = (event) => {
|
||
const file = event.target.value
|
||
const actualFile = event.target.files[0]
|
||
const fileObject = URL.createObjectURL(actualFile)
|
||
setFile(fileObject)
|
||
}
|
||
|
||
if (file !== "") {
|
||
const img = document.getElementById('logo')
|
||
var canvas = document.createElement('canvas')
|
||
canvas.width = 174
|
||
canvas.height = 174
|
||
var ctx = canvas.getContext('2d')
|
||
|
||
img.onload = function() {
|
||
// img, x, y, width, height
|
||
//ctx.drawImage(img, 174, 174)
|
||
//console.log("IMG natural: ", img.naturalWidth, img.naturalHeight)
|
||
//ctx.drawImage(img, 0, 0, 174, 174)
|
||
ctx.drawImage(img,
|
||
0, 0, img.width, img.height,
|
||
0, 0, canvas.width, canvas.height
|
||
)
|
||
|
||
try {
|
||
const canvasUrl = canvas.toDataURL()
|
||
if (canvasUrl !== fileBase64) {
|
||
//console.log("SET URL TO: ", canvasUrl)
|
||
setFileBase64(canvasUrl)
|
||
}
|
||
} catch (e) {
|
||
alert.error("Failed to parse canvasurl!")
|
||
}
|
||
}
|
||
|
||
//console.log(img.width)
|
||
//console.log(img.width)
|
||
//canvas.width = img.width
|
||
//canvas.height = img.height
|
||
}
|
||
|
||
const [imageUploadError, setImageUploadError] = useState("");
|
||
const [openImageModal, setOpenImageModal] = useState("");
|
||
const [scale, setScale] = useState(1);
|
||
const [rotate, setRotatation] = useState(0);
|
||
const [disableImageUpload, setDisableImageUpload] = useState(true);
|
||
|
||
let imageData = fileBase64;
|
||
let croppedData = file.length > 0 ? file : fileBase64
|
||
|
||
const imageInfo = <img crossorigin="anonymous" src={imageData} id="logo" style={{maxWidth: 174, maxHeight: 174, minWidth: 174, minHeight: 174, objectFit: "contain",}} />
|
||
|
||
const alternateImg = <AddPhotoAlternateIcon style={{ width: 100, height: 100, flex: "1", display: "flex", flexDirection: "row", margin: "auto", marginTop: 30, marginLeft: 40,}} onClick={() => {
|
||
upload.click()
|
||
}}/>
|
||
|
||
const zoomIn = () => {
|
||
console.log("ZOOOMING IN")
|
||
setScale(scale+0.1);
|
||
}
|
||
|
||
const zoomOut = () => {
|
||
setScale(scale-0.1);
|
||
}
|
||
const rotatation = () => {
|
||
setRotatation(rotate+10);
|
||
}
|
||
|
||
const onPositionChange = () => {
|
||
setDisableImageUpload(false);
|
||
}
|
||
|
||
const onCancelSaveAppIcon = () => {
|
||
setFile("");
|
||
setOpenImageModal(false)
|
||
setImageUploadError("")
|
||
}
|
||
|
||
let editor;
|
||
const setEditorRef = (imgEditor) => { editor = imgEditor; }
|
||
|
||
const onSaveAppIcon = () => {
|
||
if(editor){
|
||
try {
|
||
setFile("");
|
||
const canvas = editor.getImageScaledToCanvas();
|
||
setFileBase64(canvas.toDataURL())
|
||
setOpenImageModal(false)
|
||
setDisableImageUpload(true);
|
||
} catch (e) {
|
||
alert.error("Failed to set image. Replace it if this persists.")
|
||
}
|
||
}
|
||
}
|
||
|
||
const errorText = imageUploadError.length > 0 ? <div style={{marginTop: 10}}>Error: {imageUploadError}</div> : null
|
||
const imageUploadModalView = openImageModal ?
|
||
<Dialog open={openImageModal} onClose={onCancelSaveAppIcon}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: "white",
|
||
minWidth: "300px",
|
||
minHeight: "300px",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle><div style={{color: "rgba(255,255,255,0.9)"}}>Upload App Icon</div></DialogTitle>
|
||
{errorText}
|
||
<DialogContent style={{color: "rgba(255,255,255,0.65)"}}>
|
||
<AvatarEditor
|
||
ref={setEditorRef}
|
||
image={croppedData}
|
||
width={174}
|
||
height={174}
|
||
border={50}
|
||
color={[0, 0, 0, 0.6]} // RGBA
|
||
scale={scale}
|
||
rotate={rotate}
|
||
onImageChange={onPositionChange}
|
||
onLoadSuccess={()=>setRotatation(0)}
|
||
/>
|
||
<Divider style={dividerStyle}/>
|
||
<Tooltip title={"New Icon"}>
|
||
<Button variant="outlined" component="label" color="primary" style={appIconStyle}>
|
||
<AddAPhotoOutlinedIcon onClick={() => {upload.click()}} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title={"Zoom In"}>
|
||
<Button variant="outlined" component="label" color="primary" style={appIconStyle}>
|
||
<ZoomInOutlinedIcon onClick={zoomIn} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title={"Zoom out"}>
|
||
<Button variant="outlined" component="label" color="primary" style={appIconStyle}>
|
||
<ZoomOutOutlinedIcon onClick={zoomOut} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title={"Rotate"}>
|
||
<Button variant="outlined" component="label" color="primary" style={appIconStyle}>
|
||
<LoopIcon onClick={rotatation} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Divider style={dividerStyle} />
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button style={{borderRadius: "0px"}} onClick={onCancelSaveAppIcon} color="primary">
|
||
Cancel
|
||
</Button>
|
||
<Button variant="contained" style={{borderRadius: "0px"}} disabled={disableImageUpload} onClick={() => {
|
||
onSaveAppIcon()
|
||
}} color="primary">
|
||
Continue
|
||
</Button>
|
||
</DialogActions>
|
||
{/*<ParsedActionHandler />*/}
|
||
</FormControl>
|
||
</Dialog>
|
||
: null;
|
||
|
||
// Random names for type & autoComplete. Didn't research :^)
|
||
const landingpageDataBrowser =
|
||
<div style={{paddingBottom: 100, color: "white",}}>
|
||
<Breadcrumbs aria-label="breadcrumb" separator="›" style={{color: "white",}}>
|
||
<Link to="/apps" style={{textDecoration: "none", color: "inherit",}}>
|
||
<h2 style={{color: "rgba(255,255,255,0.5)"}}>
|
||
<AppsIcon style={{marginRight: 10}} />
|
||
Apps
|
||
</h2>
|
||
</Link>
|
||
<h2>
|
||
{name} {actions === null || actions === undefined || actions.length === 0 ? null : <span>({actions.length})</span>}
|
||
</h2>
|
||
</Breadcrumbs>
|
||
{imageUploadModalView}
|
||
<input hidden type="file" ref={(ref) => upload = ref} onChange={editHeaderImage} />
|
||
<Paper style={boxStyle}>
|
||
<h2 style={{marginBottom: "10px", color: "white"}}>General information</h2>
|
||
<a target="_blank" href="https://shuffler.io/docs/app_creation#app-creator-instructions" style={{textDecoration: "none", color: "#f85a3e"}}>Click here to learn more about app creation</a>
|
||
<div style={{color: "white", flex: "1", display: "flex", flexDirection: "row"}}>
|
||
<Tooltip title="Click to edit the app's image" placement="bottom">
|
||
<div style={{flex: "1", margin: 10, border: "1px solid #f85a3e", cursor: "pointer", backgroundColor: inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {
|
||
/*
|
||
if (fileBase64.length === 0) {
|
||
upload.click()
|
||
}
|
||
*/
|
||
|
||
setOpenImageModal(true)
|
||
}}>
|
||
{!imageData && (alternateImg)}
|
||
{imageInfo}
|
||
<input hidden type="file" ref={(ref) => upload = ref} onChange={editHeaderImage} />
|
||
</div>
|
||
</Tooltip>
|
||
<div style={{flex: "3", color: "white",}}>
|
||
<div style={{marginTop: "10px"}}/>
|
||
Name
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginTop: "5px", marginRight: "15px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
placeholder="Name"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={name}
|
||
onChange={e => {
|
||
const invalid = ["#", ":", "."]
|
||
for (var key in invalid) {
|
||
if (e.target.value.includes(invalid[key])) {
|
||
alert.error("Can't use "+invalid[key]+" in name")
|
||
return
|
||
}
|
||
}
|
||
|
||
if (e.target.value.length > 29) {
|
||
alert.error("Choose a shorter name (max 29).")
|
||
return
|
||
}
|
||
|
||
//e.target.value.trim()
|
||
|
||
setName(e.target.value)
|
||
}}
|
||
onBlur={e => {
|
||
setName(e.target.value.trim())
|
||
}}
|
||
color="primary"
|
||
InputProps={{
|
||
style:{
|
||
color: "white",
|
||
height: "50px",
|
||
fontSize: "1em",
|
||
},
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
}}
|
||
/>
|
||
<div style={{marginTop: "10px"}}/>
|
||
Description
|
||
<TextField
|
||
required
|
||
style={{flex: "1", marginTop: "5px", marginRight: "15px", backgroundColor: inputColor}}
|
||
fullWidth={true}
|
||
type="name"
|
||
id="outlined-with-placeholder"
|
||
margin="normal"
|
||
variant="outlined"
|
||
placeholder="A description for the service"
|
||
value={description}
|
||
onChange={e => setDescription(e.target.value)}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
color: "white",
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<Divider style={{marginBottom: 10, marginTop: 30, height: 1, width: "100%", backgroundColor: "grey"}}/>
|
||
<Typography variant="h6" style={{marginTop: 10, marginBottom: 10, color: "white",}}>API information</Typography>
|
||
<Typography variant="body1">Base URL - used as suggestion to the user</Typography>
|
||
<TextField
|
||
color="primary"
|
||
style={{backgroundColor: inputColor, marginTop: "5px"}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style:{
|
||
height: "50px",
|
||
color: "white",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
required
|
||
fullWidth={true}
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={baseUrl}
|
||
helperText={<span style={{color:"white", marginBottom: "2px",}}>Must start with http(s):// and CANT end with /. </span>}
|
||
placeholder="https://api.example.com"
|
||
onChange={e => setBaseUrl(e.target.value)}
|
||
onBlur={(event) => {
|
||
var tmpstring = event.target.value.trim()
|
||
if (tmpstring.endsWith("/")) {
|
||
tmpstring = tmpstring.slice(0, -1)
|
||
}
|
||
if (tmpstring.length > 4 && !tmpstring.startsWith("http") && !tmpstring.startsWith("ftp")) {
|
||
alert.error("URL must start with http(s)://")
|
||
}
|
||
|
||
//if (authenticationOption === "No authentication" &&
|
||
|
||
setBaseUrl(tmpstring)
|
||
}}
|
||
/>
|
||
<FormControl style={{marginTop: 30,}} variant="outlined">
|
||
<Typography variant="body1">Authentication type</Typography>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setAuthenticationOption(e.target.value)
|
||
if (e.target.value === "No authentication") {
|
||
setAuthenticationRequired(false)
|
||
} else {
|
||
setAuthenticationRequired(true)
|
||
}
|
||
}}
|
||
value={authenticationOption}
|
||
style={{backgroundColor: inputColor, color: "white", height: "50px"}}
|
||
>
|
||
{authenticationOptions.map((data, index) => (
|
||
<MenuItem key={index} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||
{data}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
{basicAuth}
|
||
{bearerAuth}
|
||
{apiKey}
|
||
{oauth2Auth}
|
||
{jwtAuth}
|
||
{extraKeys}
|
||
|
||
{/*authenticationOption === "No authentication" ? null :
|
||
<FormControlLabel
|
||
style={{color: "white", marginBottom: 0, marginTop: 20}}
|
||
label=<div style={{color: "white"}}>Authentication required (default true)</div>
|
||
control={<Switch checked={authenticationRequired} onChange={() => {
|
||
setAuthenticationRequired(!authenticationRequired)
|
||
}} />}
|
||
/>*/}
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
<div style={{marginTop: "25px"}}>
|
||
{actionView}
|
||
</div>
|
||
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
<div style={{marginTop: "25px"}}>
|
||
{tagView}
|
||
</div>
|
||
{/*
|
||
{/*
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
{testView}
|
||
*/}
|
||
|
||
<Button disabled={appBuilding} color="primary" variant="contained" style={{borderRadius: "0px", marginTop: "30px", height: "50px",}} onClick={() => {
|
||
submitApp()
|
||
}}>
|
||
{appBuilding ? <CircularProgress /> : "Save"}
|
||
</Button>
|
||
<Typography style={{marginTop: 5}}>
|
||
{errorCode.length > 0 ? `Error: ${errorCode}` : null}
|
||
</Typography>
|
||
</Paper>
|
||
</div>
|
||
|
||
|
||
const loadedCheck = isLoaded && isAppLoaded && !firstrequest ?
|
||
<div>
|
||
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
|
||
{newActionModal}
|
||
</div>
|
||
:
|
||
<div>
|
||
</div>
|
||
|
||
return(
|
||
<div>
|
||
{loadedCheck}
|
||
</div>
|
||
)
|
||
}
|
||
export default AppCreator;
|