Added more hybrid features
This commit is contained in:
@@ -863,7 +863,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
|
|||||||
tagFound := ""
|
tagFound := ""
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
for _, tag := range image.RepoTags {
|
for _, tag := range image.RepoTags {
|
||||||
log.Printf("Image: %s", tag)
|
log.Printf("[INFO] Docker Image: %s", tag)
|
||||||
|
|
||||||
if strings.ToLower(tag) == strings.ToLower(version.Name) {
|
if strings.ToLower(tag) == strings.ToLower(version.Name) {
|
||||||
img = image
|
img = image
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module shuffle
|
|||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
//replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared
|
replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared
|
||||||
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ github.com/frikky/kin-openapi v0.38.0 h1:V7ttwIJS8Vks4KL+mZVj1ZSqhIcQtgaG8akeqXE
|
|||||||
github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s=
|
github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s=
|
||||||
github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw=
|
github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw=
|
||||||
github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww=
|
github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww=
|
||||||
|
github.com/frikky/shuffle-shared v0.0.15 h1:508ceeEHfPBMCC8/K4Zve3kwRQqiXNJSw6+BDoq9X4E=
|
||||||
|
github.com/frikky/shuffle-shared v0.0.15/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww=
|
||||||
github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE=
|
github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE=
|
||||||
github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw=
|
github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw=
|
||||||
github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M=
|
github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M=
|
||||||
|
|||||||
+65
-260
@@ -932,7 +932,7 @@ func createNewUser(username, password, role, apikey string, org shuffle.Org) err
|
|||||||
neworg, err := shuffle.GetOrg(ctx, org.Id)
|
neworg, err := shuffle.GetOrg(ctx, org.Id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
//neworg.Users = append(neworg.Users, *newUser)
|
//neworg.Users = append(neworg.Users, *newUser)
|
||||||
err = setOrg(ctx, *neworg, neworg.Id)
|
err = shuffle.SetOrg(ctx, *neworg, neworg.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed updating org with user %s", newUser.Username)
|
log.Printf("Failed updating org with user %s", newUser.Username)
|
||||||
} else {
|
} else {
|
||||||
@@ -1164,7 +1164,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
userInfo, err := shuffle.HandleApiAuthentication(resp, request)
|
userInfo, err := shuffle.HandleApiAuthentication(resp, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Api authentication failed in handleInfo: %s", err)
|
log.Printf("[WARNING] Api authentication failed in handleInfo: %s", err)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(`{"success": false}`))
|
resp.Write([]byte(`{"success": false}`))
|
||||||
return
|
return
|
||||||
@@ -1572,7 +1572,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Handling login of %s", data.Username)
|
log.Printf("[INFO] Handling login of %s", data.Username)
|
||||||
|
|
||||||
err = checkUsername(data.Username)
|
err = checkUsername(data.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1675,39 +1675,6 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
|||||||
resp.Write([]byte(loginData))
|
resp.Write([]byte(loginData))
|
||||||
}
|
}
|
||||||
|
|
||||||
func setOrg(ctx context.Context, org shuffle.Org, id string) error {
|
|
||||||
// clear session_token and API_token for user
|
|
||||||
timeNow := int64(time.Now().Unix())
|
|
||||||
if org.Created == 0 {
|
|
||||||
org.Created = timeNow
|
|
||||||
}
|
|
||||||
|
|
||||||
org.Edited = timeNow
|
|
||||||
|
|
||||||
k := datastore.NameKey("Organizations", id, nil)
|
|
||||||
if _, err := dbclient.Put(ctx, k, &org); err != nil {
|
|
||||||
log.Printf("Failed setting org: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Make this update every user to have the correct org data.
|
|
||||||
//org = fixOrgUser(ctx, &org)
|
|
||||||
//_ = org
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBooks returns a list of books, ordered by title.
|
|
||||||
//func getUser(ctx context.Context, id string) (*User, error) {
|
|
||||||
// key := datastore.NameKey("Users", id, nil)
|
|
||||||
// curUser := &User{}
|
|
||||||
// if err := dbclient.Get(ctx, key, curUser); err != nil {
|
|
||||||
// return &User{}, err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return curUser, nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Index = Username
|
// Index = Username
|
||||||
func DeleteKeys(ctx context.Context, entity string, value []string) error {
|
func DeleteKeys(ctx context.Context, entity string, value []string) error {
|
||||||
// Non indexed User data
|
// Non indexed User data
|
||||||
@@ -1818,7 +1785,7 @@ func fixOrgUser(ctx context.Context, org *shuffle.Org) *shuffle.Org {
|
|||||||
// org.Users = append(org.Users, *user)
|
// org.Users = append(org.Users, *user)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// err = setOrg(ctx, *org, orgId)
|
// err = shuffle.SetOrg(ctx, *org, orgId)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// log.Printf("Failed setting org %s", orgId)
|
// log.Printf("Failed setting org %s", orgId)
|
||||||
// }
|
// }
|
||||||
@@ -1873,7 +1840,7 @@ func fixUserOrg(ctx context.Context, user *shuffle.User) *shuffle.User {
|
|||||||
org.Users = append(org.Users, *user)
|
org.Users = append(org.Users, *user)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setOrg(ctx, *org, orgId)
|
err = shuffle.SetOrg(ctx, *org, orgId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed setting org %s", orgId)
|
log.Printf("Failed setting org %s", orgId)
|
||||||
}
|
}
|
||||||
@@ -2052,7 +2019,7 @@ func handleSetHook(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
user, err := shuffle.HandleApiAuthentication(resp, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Api authentication failed in set new workflowhandler: %s", err)
|
log.Printf("[INFO] Api authentication failed in set new workflowhandler: %s", err)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(`{"success": false}`))
|
resp.Write([]byte(`{"success": false}`))
|
||||||
return
|
return
|
||||||
@@ -2362,7 +2329,7 @@ func handleDeleteSchedule(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
user, err := shuffle.HandleApiAuthentication(resp, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Api authentication failed in set new workflowhandler: %s", err)
|
log.Printf("[WARNING] Api authentication failed in set new workflowhandler: %s", err)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(`{"success": false}`))
|
resp.Write([]byte(`{"success": false}`))
|
||||||
return
|
return
|
||||||
@@ -2616,7 +2583,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeCloudAction(action CloudSyncJob, apikey string) error {
|
func executeCloudAction(action shuffle.CloudSyncJob, apikey string) error {
|
||||||
data, err := json.Marshal(action)
|
data, err := json.Marshal(action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed cloud webhook action marshalling: %s", err)
|
log.Printf("Failed cloud webhook action marshalling: %s", err)
|
||||||
@@ -2661,164 +2628,6 @@ func executeCloudAction(action CloudSyncJob, apikey string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts a new webhook
|
|
||||||
func handleNewHook(resp http.ResponseWriter, request *http.Request) {
|
|
||||||
cors := shuffle.HandleCors(resp, request)
|
|
||||||
if cors {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Api authentication failed in set new workflowhandler: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestData struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Workflow string `json:"workflow"`
|
|
||||||
Start string `json:"start"`
|
|
||||||
Environment string `json:"environment"`
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(request.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Body data error: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("Data: %s", string(body))
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
var requestdata requestData
|
|
||||||
err = yaml.Unmarshal([]byte(body), &requestdata)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed unmarshaling inputdata: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("%#v", requestdata)
|
|
||||||
|
|
||||||
// CBA making a real thing. Already had some code lol
|
|
||||||
newId := requestdata.Id
|
|
||||||
if len(newId) != 36 {
|
|
||||||
log.Printf("Bad ID")
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false, "reason": "Invalid ID"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestdata.Id == "" || requestdata.Name == "" {
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false, "reason": "Requires fields id and name can't be empty"}`))
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
validTypes := []string{
|
|
||||||
"webhook",
|
|
||||||
}
|
|
||||||
|
|
||||||
isTypeValid := false
|
|
||||||
for _, thistype := range validTypes {
|
|
||||||
if requestdata.Type == thistype {
|
|
||||||
isTypeValid = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(isTypeValid) {
|
|
||||||
log.Printf("Type %s is not valid. Try any of these: %s", requestdata.Type, strings.Join(validTypes, ", "))
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let remote endpoint handle access checks (shuffler.io)
|
|
||||||
currentUrl := fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId)
|
|
||||||
startNode := requestdata.Start
|
|
||||||
if requestdata.Environment == "cloud" {
|
|
||||||
// https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c
|
|
||||||
log.Printf("[INFO] Should START a cloud webhook for url %s for startnode %s", currentUrl, startNode)
|
|
||||||
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed finding org %s: %s", org.Id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
action := CloudSyncJob{
|
|
||||||
Type: "webhook",
|
|
||||||
Action: "start",
|
|
||||||
OrgId: org.Id,
|
|
||||||
PrimaryItemId: newId,
|
|
||||||
SecondaryItem: startNode,
|
|
||||||
ThirdItem: requestdata.Workflow,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = executeCloudAction(action, org.SyncConfig.Apikey)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed cloud action START execution: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
log.Printf("[INFO] Successfully set up cloud action schedule")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hook := Hook{
|
|
||||||
Id: newId,
|
|
||||||
Start: startNode,
|
|
||||||
Workflows: []string{requestdata.Workflow},
|
|
||||||
Info: Info{
|
|
||||||
Name: requestdata.Name,
|
|
||||||
Description: requestdata.Description,
|
|
||||||
Url: fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId),
|
|
||||||
},
|
|
||||||
Type: "webhook",
|
|
||||||
Owner: user.Username,
|
|
||||||
Status: "uninitialized",
|
|
||||||
Actions: []HookAction{
|
|
||||||
HookAction{
|
|
||||||
Type: "workflow",
|
|
||||||
Name: requestdata.Name,
|
|
||||||
Id: requestdata.Workflow,
|
|
||||||
Field: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Running: false,
|
|
||||||
OrgId: user.ActiveOrg.Id,
|
|
||||||
Environment: requestdata.Environment,
|
|
||||||
}
|
|
||||||
|
|
||||||
hook.Status = "running"
|
|
||||||
hook.Running = true
|
|
||||||
err = setHook(ctx, hook)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed setting hook: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = increaseStatisticsField(ctx, "total_workflow_triggers", requestdata.Workflow, 1, user.ActiveOrg.Id)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[INFO] Failed to increase total workflows: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[INFO] Set up a new hook with ID %s and environment %s", newId, hook.Environment)
|
|
||||||
resp.WriteHeader(200)
|
|
||||||
resp.Write([]byte(`{"success": true}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
|
func getSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||||
if request.Method != "GET" {
|
if request.Method != "GET" {
|
||||||
setSpecificSchedule(resp, request)
|
setSpecificSchedule(resp, request)
|
||||||
@@ -3462,7 +3271,7 @@ func handleGetallHooks(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
user, err := shuffle.HandleApiAuthentication(resp, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Api authentication failed in set new workflowhandler: %s", err)
|
log.Printf("[WARNING] Api authentication failed in set new workflowhandler: %s", err)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(`{"success": false}`))
|
resp.Write([]byte(`{"success": false}`))
|
||||||
return
|
return
|
||||||
@@ -3532,7 +3341,7 @@ func findAvailablePorts(startRange int64, endRange int64) string {
|
|||||||
func handleSendalert(resp http.ResponseWriter, request *http.Request) {
|
func handleSendalert(resp http.ResponseWriter, request *http.Request) {
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
user, err := shuffle.HandleApiAuthentication(resp, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Api authentication failed in sendalert: %s", err)
|
log.Printf("[WARNING] Api authentication failed in sendalert: %s", err)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(`{"success": false}`))
|
resp.Write([]byte(`{"success": false}`))
|
||||||
return
|
return
|
||||||
@@ -4753,22 +4562,6 @@ func handleAppHotload(ctx context.Context, location string, forceUpdate bool) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary = usually an outer ID, e.g. workflow ID
|
|
||||||
// Secondary = something to specify what inside workflow to execute
|
|
||||||
// Third = Some data to add to it
|
|
||||||
type CloudSyncJob struct {
|
|
||||||
Id string `json:"id" datastore:"id"`
|
|
||||||
Type string `json:"type" datastore:"type"`
|
|
||||||
Action string `json:"action" datastore:"action"`
|
|
||||||
OrgId string `json:"org_id" datastore:"org_id"`
|
|
||||||
PrimaryItemId string `json:"primary_item_id" datastore:"primary_item_id"`
|
|
||||||
SecondaryItem string `json:"secondary_item" datastore:"secondary_item"`
|
|
||||||
ThirdItem string `json:"third_item" datastore:"third_item"`
|
|
||||||
FourthItem string `json:"fourth_item" datastore:"fourth_item"`
|
|
||||||
FifthItem string `json:"fifth_item" datastore:"fifth_item"`
|
|
||||||
Created string `json:"created" datastore:"created"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executionArgument string) error {
|
func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executionArgument string) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// 1. Get the workflow
|
// 1. Get the workflow
|
||||||
@@ -4826,7 +4619,7 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloudJob(job CloudSyncJob) error {
|
func handleCloudJob(job shuffle.CloudSyncJob) error {
|
||||||
// May need authentication in all of these..?
|
// May need authentication in all of these..?
|
||||||
|
|
||||||
log.Printf("[INFO] Handle job with type %s and action %s", job.Type, job.Action)
|
log.Printf("[INFO] Handle job with type %s and action %s", job.Type, job.Action)
|
||||||
@@ -4932,7 +4725,7 @@ func handleCloudJob(job CloudSyncJob) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fullUrl := fmt.Sprintf("https://shuffler.io/api/v1/workflows/%s/execute?authorization=%s&start=%s&reference_execution=%s&answer=true", job.PrimaryItemId, job.FourthItem, job.SecondaryItem, job.ThirdItem)
|
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute?authorization=%s&start=%s&reference_execution=%s&answer=true", syncUrl, job.PrimaryItemId, job.FourthItem, job.SecondaryItem, job.ThirdItem)
|
||||||
newRequest, err := http.NewRequest(
|
newRequest, err := http.NewRequest(
|
||||||
"GET",
|
"GET",
|
||||||
fullUrl,
|
fullUrl,
|
||||||
@@ -4997,9 +4790,9 @@ func handleCloudJob(job CloudSyncJob) error {
|
|||||||
// Handles jobs from remote (cloud)
|
// Handles jobs from remote (cloud)
|
||||||
func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
||||||
type retStruct struct {
|
type retStruct struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Jobs []CloudSyncJob `json:"jobs"`
|
Jobs []shuffle.CloudSyncJob `json:"jobs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData := retStruct{}
|
responseData := retStruct{}
|
||||||
@@ -5010,9 +4803,9 @@ func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if !responseData.Success {
|
if !responseData.Success {
|
||||||
log.Printf("Should stop org job controller because no success?")
|
log.Printf("[WARNING] Should stop org job controller because no success?")
|
||||||
|
|
||||||
if strings.Contains(responseData.Reason, "Bad apikey") || strings.Contains(responseData.Reason, "Error getting the organization") {
|
if strings.Contains(responseData.Reason, "Bad apikey") || strings.Contains(responseData.Reason, "Error getting the organization") || strings.Contains(responseData.Reason, "Organization isn't syncing") {
|
||||||
log.Printf("[WARNING] Remote error; Bad apikey or org error. Stopping sync for org: %s", responseData.Reason)
|
log.Printf("[WARNING] Remote error; Bad apikey or org error. Stopping sync for org: %s", responseData.Reason)
|
||||||
|
|
||||||
if value, exists := scheduledOrgs[org.Id]; exists {
|
if value, exists := scheduledOrgs[org.Id]; exists {
|
||||||
@@ -5029,7 +4822,20 @@ func remoteOrgJobController(org shuffle.Org, body []byte) error {
|
|||||||
org.SyncConfig.Interval = 0
|
org.SyncConfig.Interval = 0
|
||||||
org.SyncConfig.Apikey = ""
|
org.SyncConfig.Apikey = ""
|
||||||
org.CloudSync = false
|
org.CloudSync = false
|
||||||
err = setOrg(ctx, *org, org.Id)
|
|
||||||
|
// Just in case
|
||||||
|
org, err = handleStopCloudSync(syncUrl, *org)
|
||||||
|
|
||||||
|
startDate := time.Now().Unix()
|
||||||
|
org.SyncFeatures.Webhook = shuffle.SyncData{Active: false, Type: "trigger", Name: "Webhook", StartDate: startDate}
|
||||||
|
org.SyncFeatures.UserInput = shuffle.SyncData{Active: false, Type: "trigger", Name: "User Input", StartDate: startDate}
|
||||||
|
org.SyncFeatures.EmailTrigger = shuffle.SyncData{Active: false, Type: "action", Name: "Email Trigger", StartDate: startDate}
|
||||||
|
org.SyncFeatures.Schedules = shuffle.SyncData{Active: false, Type: "trigger", Name: "Schedule", StartDate: startDate, Limit: 0}
|
||||||
|
org.SyncFeatures.SendMail = shuffle.SyncData{Active: false, Type: "action", Name: "Send Email", StartDate: startDate, Limit: 0}
|
||||||
|
org.SyncFeatures.SendSms = shuffle.SyncData{Active: false, Type: "action", Name: "Send SMS", StartDate: startDate, Limit: 0}
|
||||||
|
org.CloudSyncActive = false
|
||||||
|
|
||||||
|
err = shuffle.SetOrg(ctx, *org, org.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARNING] Failed setting organization when stopping sync: %s", err)
|
log.Printf("[WARNING] Failed setting organization when stopping sync: %s", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -5082,8 +4888,7 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf("Data: %s", respBody)
|
//log.Printf("Remote Data: %s", respBody)
|
||||||
|
|
||||||
err = remoteOrgJobController(org, respBody)
|
err = remoteOrgJobController(org, respBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed job controller run for %s: %s", respBody, err)
|
log.Printf("[ERROR] Failed job controller run for %s: %s", respBody, err)
|
||||||
@@ -5126,7 +4931,7 @@ func runInit(ctx context.Context) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 15 second timeout
|
// 15 second timeout
|
||||||
Timeout: 15 * time.Second,
|
Timeout: 15 * 15time.Second,
|
||||||
|
|
||||||
// don't follow redirect
|
// don't follow redirect
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
@@ -5172,7 +4977,7 @@ func runInit(ctx context.Context) {
|
|||||||
CloudSync: false,
|
CloudSync: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setOrg(ctx, newOrg, orgId)
|
err = shuffle.SetOrg(ctx, newOrg, orgId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed setting organization: %s", err)
|
log.Printf("Failed setting organization: %s", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -5219,7 +5024,7 @@ func runInit(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if setOrgBool {
|
if setOrgBool {
|
||||||
err = setOrg(ctx, activeOrg, activeOrg.Id)
|
err = shuffle.SetOrg(ctx, activeOrg, activeOrg.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed setting org %s: %s!", activeOrg.Name, err)
|
log.Printf("Failed setting org %s: %s!", activeOrg.Name, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -5741,9 +5546,9 @@ func handleVerifyCloudsync(orgId string) (shuffle.SyncFeatures, error) {
|
|||||||
|
|
||||||
// Actually stops syncing with cloud for an org.
|
// Actually stops syncing with cloud for an org.
|
||||||
// Disables potential schedules, removes environments, breaks workflows etc.
|
// Disables potential schedules, removes environments, breaks workflows etc.
|
||||||
func handleStopCloudSync(syncUrl string, org shuffle.Org) error {
|
func handleStopCloudSync(syncUrl string, org shuffle.Org) (*shuffle.Org, error) {
|
||||||
if len(org.SyncConfig.Apikey) == 0 {
|
if len(org.SyncConfig.Apikey) == 0 {
|
||||||
return errors.New(fmt.Sprintf("Couldn't find any sync key to disable org %s", org.Id))
|
return &org, errors.New(fmt.Sprintf("Couldn't find any sync key to disable org %s", org.Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Should run cloud sync disable for org %s with URL %s and sync key %s", org.Id, syncUrl, org.SyncConfig.Apikey)
|
log.Printf("Should run cloud sync disable for org %s with URL %s and sync key %s", org.Id, syncUrl, org.SyncConfig.Apikey)
|
||||||
@@ -5758,28 +5563,28 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) error {
|
|||||||
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey))
|
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey))
|
||||||
newresp, err := client.Do(req)
|
newresp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &org, err
|
||||||
}
|
}
|
||||||
|
|
||||||
respBody, err := ioutil.ReadAll(newresp.Body)
|
respBody, err := ioutil.ReadAll(newresp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &org, err
|
||||||
}
|
}
|
||||||
log.Printf("Remote disable ret: %s", string(respBody))
|
log.Printf("Remote disable ret: %s", string(respBody))
|
||||||
|
|
||||||
responseData := retStruct{}
|
responseData := retStruct{}
|
||||||
err = json.Unmarshal(respBody, &responseData)
|
err = json.Unmarshal(respBody, &responseData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &org, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if newresp.StatusCode != 200 {
|
if newresp.StatusCode != 200 {
|
||||||
return errors.New(fmt.Sprintf("Got status code %d when disabling org remotely. Expected 200. Contact support.", newresp.StatusCode))
|
return &org, errors.New(fmt.Sprintf("Got status code %d when disabling org remotely. Expected 200. Contact support.", newresp.StatusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !responseData.Success {
|
if !responseData.Success {
|
||||||
//log.Printf("Success reason: %s", responseData.Reason)
|
//log.Printf("Success reason: %s", responseData.Reason)
|
||||||
return errors.New(responseData.Reason)
|
return &org, errors.New(responseData.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Everything is success. Should disable org sync for %s", org.Id)
|
log.Printf("Everything is success. Should disable org sync for %s", org.Id)
|
||||||
@@ -5789,18 +5594,18 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) error {
|
|||||||
org.SyncFeatures = shuffle.SyncFeatures{}
|
org.SyncFeatures = shuffle.SyncFeatures{}
|
||||||
org.SyncConfig = shuffle.SyncConfig{}
|
org.SyncConfig = shuffle.SyncConfig{}
|
||||||
|
|
||||||
err = setOrg(ctx, org, org.Id)
|
err = shuffle.SetOrg(ctx, org, org.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newerror := fmt.Sprintf("ERROR: Failed updating even though there was success: %s", err)
|
newerror := fmt.Sprintf("ERROR: Failed updating even though there was success: %s", err)
|
||||||
log.Printf(newerror)
|
log.Printf(newerror)
|
||||||
return errors.New(newerror)
|
return &org, errors.New(newerror)
|
||||||
}
|
}
|
||||||
|
|
||||||
var environments []shuffle.Environment
|
var environments []shuffle.Environment
|
||||||
q := datastore.NewQuery("Environments").Filter("org_id =", org.Id)
|
q := datastore.NewQuery("Environments").Filter("org_id =", org.Id)
|
||||||
_, err = dbclient.GetAll(ctx, q, &environments)
|
_, err = dbclient.GetAll(ctx, q, &environments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &org, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't disable, this will be deleted entirely
|
// Don't disable, this will be deleted entirely
|
||||||
@@ -5810,9 +5615,9 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) error {
|
|||||||
environment.Archived = true
|
environment.Archived = true
|
||||||
err = setEnvironment(ctx, &environment)
|
err = setEnvironment(ctx, &environment)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Printf("Updated cloud environment %s", environment.Name)
|
log.Printf("[INFO] Updated cloud environment %s", environment.Name)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Failed to update cloud environment %s", environment.Name)
|
log.Printf("[INFO] Failed to update cloud environment %s", environment.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5820,12 +5625,12 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) error {
|
|||||||
// FIXME: This doesn't work?
|
// FIXME: This doesn't work?
|
||||||
if value, exists := scheduledOrgs[org.Id]; exists {
|
if value, exists := scheduledOrgs[org.Id]; exists {
|
||||||
// Looks like this does the trick? Hurr
|
// Looks like this does the trick? Hurr
|
||||||
log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id)
|
log.Printf("[WARNING] STOPPING ORG SCHEDULE for: %s", org.Id)
|
||||||
|
|
||||||
value.Lock()
|
value.Lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return &org, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// INFO: https://docs.google.com/drawings/d/1JJebpPeEVEbmH_qsAC6zf9Noygp7PytvesrkhE19QrY/edit
|
// INFO: https://docs.google.com/drawings/d/1JJebpPeEVEbmH_qsAC6zf9Noygp7PytvesrkhE19QrY/edit
|
||||||
@@ -5885,13 +5690,13 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Check if user is admin of this org
|
// FIXME: Check if user is admin of this org
|
||||||
log.Printf("Checking org %s", org.Name)
|
//log.Printf("Checking org %s", org.Name)
|
||||||
userFound := false
|
userFound := false
|
||||||
admin := false
|
admin := false
|
||||||
for _, inneruser := range org.Users {
|
for _, inneruser := range org.Users {
|
||||||
if inneruser.Id == user.Id {
|
if inneruser.Id == user.Id {
|
||||||
userFound = true
|
userFound = true
|
||||||
log.Printf("Role: %s", inneruser.Role)
|
//log.Printf("[INFO] Role: %s", inneruser.Role)
|
||||||
if inneruser.Role == "admin" {
|
if inneruser.Role == "admin" {
|
||||||
admin = true
|
admin = true
|
||||||
}
|
}
|
||||||
@@ -5923,17 +5728,17 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
apiPath := "/api/v1/cloud/sync/setup"
|
apiPath := "/api/v1/cloud/sync/setup"
|
||||||
if tmpData.Disable {
|
if tmpData.Disable {
|
||||||
if !org.CloudSync {
|
if !org.CloudSync {
|
||||||
log.Printf("Org %s isn't syncing. Can't stop.", org.Id)
|
log.Printf("[WARNING] Org %s isn't syncing. Can't stop.", org.Id)
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Skipped cloud sync setup. Already syncing."}`)))
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Skipped cloud sync setup. Already syncing."}`)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Should disable sync for org %s", org.Id)
|
log.Printf("[INFO] Should disable sync for org %s", org.Id)
|
||||||
apiPath := "/api/v1/cloud/sync/stop"
|
apiPath := "/api/v1/cloud/sync/stop"
|
||||||
syncPath := fmt.Sprintf("%s%s", syncUrl, apiPath)
|
syncPath := fmt.Sprintf("%s%s", syncUrl, apiPath)
|
||||||
|
|
||||||
err = handleStopCloudSync(syncPath, *org)
|
_, err = handleStopCloudSync(syncPath, *org)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.WriteHeader(401)
|
resp.WriteHeader(401)
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
||||||
@@ -6027,7 +5832,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interval := int(responseData.IntervalSeconds)
|
interval := int(responseData.IntervalSeconds)
|
||||||
log.Printf("Starting cloud sync on interval %d", interval)
|
log.Printf("[INFO] Starting cloud sync on interval %d", interval)
|
||||||
job := func() {
|
job := func() {
|
||||||
err := remoteOrgJobHandler(*org, interval)
|
err := remoteOrgJobHandler(*org, interval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6039,13 +5844,13 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[CRITICAL] Failed to schedule org: %s", err)
|
log.Printf("[CRITICAL] Failed to schedule org: %s", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Started sync on interval %d for org %s", interval, org.Name)
|
log.Printf("[INFO] Started sync on interval %d for org %s", interval, org.Name)
|
||||||
scheduledOrgs[org.Id] = jobret
|
scheduledOrgs[org.Id] = jobret
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Add this for every feature
|
// FIXME: Add this for every feature
|
||||||
if org.SyncFeatures.Workflows.Active {
|
if org.SyncFeatures.Workflows.Active {
|
||||||
log.Printf("Should activate cloud workflows for org %s!", org.Id)
|
log.Printf("[INFO] Should activate cloud workflows for org %s!", org.Id)
|
||||||
|
|
||||||
// 1. Find environment
|
// 1. Find environment
|
||||||
// 2. If cloud env found, enable it (un-archive)
|
// 2. If cloud env found, enable it (un-archive)
|
||||||
@@ -6063,9 +5868,9 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
environment.Archived = false
|
environment.Archived = false
|
||||||
err = setEnvironment(ctx, &environment)
|
err = setEnvironment(ctx, &environment)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Printf("Re-added cloud environment %s", environment.Name)
|
log.Printf("[INFO] Re-added cloud environment %s", environment.Name)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Failed to re-enable cloud environment %s", environment.Name)
|
log.Printf("[INFO] Failed to re-enable cloud environment %s", environment.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
found = true
|
found = true
|
||||||
@@ -6074,7 +5879,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
log.Printf("Env for cloud not found. Should add it!")
|
log.Printf("[INFO] Env for cloud not found. Should add it!")
|
||||||
newEnv := shuffle.Environment{
|
newEnv := shuffle.Environment{
|
||||||
Name: "Cloud",
|
Name: "Cloud",
|
||||||
Type: "cloud",
|
Type: "cloud",
|
||||||
@@ -6096,7 +5901,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setOrg(ctx, *org, org.Id)
|
err = shuffle.SetOrg(ctx, *org, org.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ERROR: Failed updating org even though there was success: %s", err)
|
log.Printf("ERROR: Failed updating org even though there was success: %s", err)
|
||||||
resp.WriteHeader(400)
|
resp.WriteHeader(400)
|
||||||
@@ -6106,7 +5911,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
if responseData.IntervalSeconds > 0 {
|
if responseData.IntervalSeconds > 0 {
|
||||||
// FIXME:
|
// FIXME:
|
||||||
log.Printf("Should set up interval for %d with session key %s for org %s", responseData.IntervalSeconds, responseData.SessionKey, org.Name)
|
log.Printf("[INFO] Should set up interval for %d with session key %s for org %s", responseData.IntervalSeconds, responseData.SessionKey, org.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.WriteHeader(200)
|
resp.WriteHeader(200)
|
||||||
@@ -6220,13 +6025,13 @@ func initHandlers() {
|
|||||||
r.HandleFunc("/api/v1/workflows/{key}/outlook", createOutlookSub).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v1/workflows/{key}/outlook", createOutlookSub).Methods("POST", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", handleDeleteOutlookSub).Methods("DELETE", "OPTIONS")
|
r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", handleDeleteOutlookSub).Methods("DELETE", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/workflows/{key}/executions", getWorkflowExecutions).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v1/workflows/{key}/executions", getWorkflowExecutions).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/workflows/{key}/executions/{key}/abort", abortExecution).Methods("GET", "OPTIONS")
|
r.HandleFunc("/api/v1/workflows/{key}/executions/{key}/abort", shuffle.AbortExecution).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/workflows/{key}", deleteWorkflow).Methods("DELETE", "OPTIONS")
|
r.HandleFunc("/api/v1/workflows/{key}", deleteWorkflow).Methods("DELETE", "OPTIONS")
|
||||||
|
|
||||||
// Triggers
|
// Triggers
|
||||||
r.HandleFunc("/api/v1/hooks/new", handleNewHook).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v1/hooks/new", shuffle.HandleNewHook).Methods("POST", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/hooks/{key}", handleWebhookCallback).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v1/hooks/{key}", handleWebhookCallback).Methods("POST", "OPTIONS")
|
||||||
r.HandleFunc("/api/v1/hooks/{key}/delete", handleDeleteHook).Methods("DELETE", "OPTIONS")
|
r.HandleFunc("/api/v1/hooks/{key}/delete", shuffle.HandleDeleteHook).Methods("DELETE", "OPTIONS")
|
||||||
|
|
||||||
// OpenAPI configuration
|
// OpenAPI configuration
|
||||||
r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS")
|
r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS")
|
||||||
|
|||||||
@@ -817,7 +817,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
log.Printf("[INFO] Starting cloud configuration TO START trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId)
|
log.Printf("[INFO] Starting cloud configuration TO START trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId)
|
||||||
|
|
||||||
action := CloudSyncJob{
|
action := shuffle.CloudSyncJob{
|
||||||
Type: "outlook",
|
Type: "outlook",
|
||||||
Action: "start",
|
Action: "start",
|
||||||
OrgId: org.Id,
|
OrgId: org.Id,
|
||||||
@@ -1218,7 +1218,7 @@ func handleOutlookSubRemoval(ctx context.Context, user shuffle.User, workflowId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Stopping cloud configuration for trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId)
|
log.Printf("[INFO] Stopping cloud configuration for trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId)
|
||||||
action := CloudSyncJob{
|
action := shuffle.CloudSyncJob{
|
||||||
Type: "outlook",
|
Type: "outlook",
|
||||||
Action: "stop",
|
Action: "stop",
|
||||||
OrgId: org.Id,
|
OrgId: org.Id,
|
||||||
|
|||||||
+6
-330
@@ -1894,211 +1894,6 @@ func getWorkflowLocal(fileId string, request *http.Request) ([]byte, error) {
|
|||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func abortExecution(resp http.ResponseWriter, request *http.Request) {
|
|
||||||
cors := handleCors(resp, request)
|
|
||||||
if cors {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("\n\nINSIDE ABORT\n\n")
|
|
||||||
|
|
||||||
location := strings.Split(request.URL.String(), "/")
|
|
||||||
var fileId string
|
|
||||||
if location[1] == "api" {
|
|
||||||
if len(location) <= 4 {
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileId = location[4]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fileId) != 36 {
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false, "reason": "Workflow ID to abort is not valid"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
executionId := location[6]
|
|
||||||
if len(executionId) != 36 {
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false, "reason": "ExecutionID not valid"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, executionId)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[ERROR] Failed getting execution (abort) %s: %s", executionId, err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist (abort)."}`, executionId)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apikey := request.Header.Get("Authorization")
|
|
||||||
parsedKey := ""
|
|
||||||
if strings.HasPrefix(apikey, "Bearer ") {
|
|
||||||
apikeyCheck := strings.Split(apikey, " ")
|
|
||||||
if len(apikeyCheck) == 2 {
|
|
||||||
parsedKey = apikeyCheck[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if workflowExecution.Authorization != parsedKey {
|
|
||||||
// FIXME: Check the execution if this fails.
|
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Api authentication failed in abort workflow: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME - have a check for org etc too..
|
|
||||||
if user.Id != workflowExecution.Workflow.Owner {
|
|
||||||
log.Printf("[INFO] Wrong user (%s) for workflowexecution workflow %s", user.Username, workflowExecution.Workflow.ID)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//log.Printf("[INFO] API key to abort/finish execution %s is correct.", executionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" || workflowExecution.Status == "FINISHED" {
|
|
||||||
log.Printf("[INFO] Stopped execution of %s with status %s", executionId, workflowExecution.Status)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Status for %s is %s, which can't be aborted."}`, executionId, workflowExecution.Status)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
topic := "workflowexecution"
|
|
||||||
|
|
||||||
workflowExecution.CompletedAt = int64(time.Now().Unix())
|
|
||||||
workflowExecution.Status = "ABORTED"
|
|
||||||
log.Printf("[INFO] Running shutdown of %s", workflowExecution.ExecutionId)
|
|
||||||
|
|
||||||
lastResult := ""
|
|
||||||
newResults := []shuffle.ActionResult{}
|
|
||||||
// type ActionResult struct {
|
|
||||||
for _, result := range workflowExecution.Results {
|
|
||||||
if result.Status == "EXECUTING" {
|
|
||||||
result.Status = "ABORTED"
|
|
||||||
result.Result = "Aborted because of error in another node (1)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result.Result) > 0 {
|
|
||||||
lastResult = result.Result
|
|
||||||
}
|
|
||||||
|
|
||||||
newResults = append(newResults, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
workflowExecution.Results = newResults
|
|
||||||
if len(workflowExecution.Result) == 0 {
|
|
||||||
workflowExecution.Result = lastResult
|
|
||||||
}
|
|
||||||
|
|
||||||
addResult := true
|
|
||||||
for _, result := range workflowExecution.Results {
|
|
||||||
if result.Status != "SKIPPED" {
|
|
||||||
addResult = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := 0
|
|
||||||
for _, trigger := range workflowExecution.Workflow.Triggers {
|
|
||||||
//log.Printf("Appname trigger (0): %s", trigger.AppName)
|
|
||||||
if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" {
|
|
||||||
extra += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedReason := "An error occurred during execution of this node"
|
|
||||||
reason, reasonok := request.URL.Query()["reason"]
|
|
||||||
if reasonok {
|
|
||||||
parsedReason = reason[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(workflowExecution.Results) == 0 || addResult {
|
|
||||||
newaction := shuffle.Action{
|
|
||||||
ID: workflowExecution.Start,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, action := range workflowExecution.Workflow.Actions {
|
|
||||||
if action.ID == workflowExecution.Start {
|
|
||||||
newaction = action
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
workflowExecution.Results = append(workflowExecution.Results, shuffle.ActionResult{
|
|
||||||
Action: newaction,
|
|
||||||
ExecutionId: workflowExecution.ExecutionId,
|
|
||||||
Authorization: workflowExecution.Authorization,
|
|
||||||
Result: parsedReason,
|
|
||||||
StartedAt: workflowExecution.StartedAt,
|
|
||||||
CompletedAt: workflowExecution.StartedAt,
|
|
||||||
Status: "FAILURE",
|
|
||||||
})
|
|
||||||
} else if len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions)+extra {
|
|
||||||
log.Printf("[INFO] DONE - Nothing to add during abort!")
|
|
||||||
} else {
|
|
||||||
//log.Printf("VALIDATING INPUT!")
|
|
||||||
node, nodeok := request.URL.Query()["node"]
|
|
||||||
if nodeok {
|
|
||||||
nodeId := node[0]
|
|
||||||
log.Printf("[INFO] Found abort node %s", nodeId)
|
|
||||||
newaction := shuffle.Action{
|
|
||||||
ID: nodeId,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, action := range workflowExecution.Workflow.Actions {
|
|
||||||
if action.ID == nodeId {
|
|
||||||
newaction = action
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
workflowExecution.Results = append(workflowExecution.Results, shuffle.ActionResult{
|
|
||||||
Action: newaction,
|
|
||||||
ExecutionId: workflowExecution.ExecutionId,
|
|
||||||
Authorization: workflowExecution.Authorization,
|
|
||||||
Result: parsedReason,
|
|
||||||
StartedAt: workflowExecution.StartedAt,
|
|
||||||
CompletedAt: workflowExecution.StartedAt,
|
|
||||||
Status: "FAILURE",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error saving workflow execution for updates when aborting %s: %s", topic, err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution status to abort"}`)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = increaseStatisticsField(ctx, "workflow_executions_aborted", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to increase aborted execution stats: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME - allowed to edit it? idk
|
|
||||||
resp.WriteHeader(200)
|
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
|
|
||||||
|
|
||||||
// Not sure what's up here
|
|
||||||
//if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
|
|
||||||
// log.Printf("Workflowexecution is already aborted. No further action can be taken")
|
|
||||||
// resp.WriteHeader(401)
|
|
||||||
// resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Workflowexecution is aborted because of %s with result %s and status %s"}`, workflowExecution.LastNode, workflowExecution.Result, workflowExecution.Status)))
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
//// New execution with firestore
|
//// New execution with firestore
|
||||||
|
|
||||||
func cleanupExecutions(resp http.ResponseWriter, request *http.Request) {
|
func cleanupExecutions(resp http.ResponseWriter, request *http.Request) {
|
||||||
@@ -3002,11 +2797,10 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Schedule: %#v", schedule)
|
//log.Printf("Schedule: %#v", schedule)
|
||||||
|
|
||||||
if schedule.Environment == "cloud" {
|
if schedule.Environment == "cloud" {
|
||||||
log.Printf("[INFO] Should STOP a cloud schedule for workflow %s with schedule ID %s", fileId, scheduleId)
|
log.Printf("[INFO] Should STOP a cloud schedule for workflow %s with schedule ID %s", fileId, scheduleId)
|
||||||
// https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c
|
|
||||||
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed finding org %s: %s", org.Id, err)
|
log.Printf("Failed finding org %s: %s", org.Id, err)
|
||||||
@@ -3015,7 +2809,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
// 1. Send request to cloud
|
// 1. Send request to cloud
|
||||||
// 2. Remove schedule if success
|
// 2. Remove schedule if success
|
||||||
action := CloudSyncJob{
|
action := shuffle.CloudSyncJob{
|
||||||
Type: "schedule",
|
Type: "schedule",
|
||||||
Action: "stop",
|
Action: "stop",
|
||||||
OrgId: org.Id,
|
OrgId: org.Id,
|
||||||
@@ -3328,7 +3122,6 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
if schedule.Environment == "cloud" {
|
if schedule.Environment == "cloud" {
|
||||||
log.Printf("[INFO] Should START a cloud schedule for workflow %s with schedule ID %s", workflow.ID, schedule.Id)
|
log.Printf("[INFO] Should START a cloud schedule for workflow %s with schedule ID %s", workflow.ID, schedule.Id)
|
||||||
// https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c
|
|
||||||
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed finding org %s: %s", org.Id, err)
|
log.Printf("Failed finding org %s: %s", org.Id, err)
|
||||||
@@ -3339,7 +3132,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
|
|||||||
// 2 = schedule (cron, frequency)
|
// 2 = schedule (cron, frequency)
|
||||||
// 3 = workflowId
|
// 3 = workflowId
|
||||||
// 4 = execution argument
|
// 4 = execution argument
|
||||||
action := CloudSyncJob{
|
action := shuffle.CloudSyncJob{
|
||||||
Type: "schedule",
|
Type: "schedule",
|
||||||
Action: "start",
|
Action: "start",
|
||||||
OrgId: org.Id,
|
OrgId: org.Id,
|
||||||
@@ -5592,123 +5385,6 @@ func handleStopHook(resp http.ResponseWriter, request *http.Request) {
|
|||||||
resp.Write([]byte(`{"success": true, "reason": "Stopped webhook"}`))
|
resp.Write([]byte(`{"success": true, "reason": "Stopped webhook"}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeleteHook(resp http.ResponseWriter, request *http.Request) {
|
|
||||||
cors := handleCors(resp, request)
|
|
||||||
if cors {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := shuffle.HandleApiAuthentication(resp, request)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Api authentication failed in set new workflowhandler: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
location := strings.Split(request.URL.String(), "/")
|
|
||||||
|
|
||||||
var fileId string
|
|
||||||
if location[1] == "api" {
|
|
||||||
if len(location) <= 4 {
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileId = location[4]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fileId) != 36 {
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false, "reason": "Workflow ID when deleting hook is not valid"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
hook, err := getHook(ctx, fileId)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed getting hook %s (delete): %s", fileId, err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Id != hook.Owner && user.ActiveOrg.Id != hook.OrgId {
|
|
||||||
log.Printf("Wrong user (%s) for workflow %s", user.Username, hook.Id)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hook.Workflows) > 0 {
|
|
||||||
//err = increaseStatisticsField(ctx, "total_workflow_triggers", hook.Workflows[0], -1, user.ActiveOrg.Id)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Printf("Failed to increase total workflows: %s", err)
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
hook.Status = "stopped"
|
|
||||||
err = setHook(ctx, *hook)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed setting hook: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(`{"success": false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Hook: %#v", hook)
|
|
||||||
if hook.Environment == "cloud" {
|
|
||||||
log.Printf("[INFO] Should STOP cloud webhook https://shuffler.io/api/v1/hooks/webhook_%s", hook.Id)
|
|
||||||
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed finding org %s: %s", org.Id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
action := CloudSyncJob{
|
|
||||||
Type: "webhook",
|
|
||||||
Action: "stop",
|
|
||||||
OrgId: org.Id,
|
|
||||||
PrimaryItemId: hook.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hook.Workflows) > 0 {
|
|
||||||
action.SecondaryItem = hook.Workflows[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
err = executeCloudAction(action, org.SyncConfig.Apikey)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed cloud action STOP execution: %s", err)
|
|
||||||
resp.WriteHeader(401)
|
|
||||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is here to force stop and remove the old webhook
|
|
||||||
//image := "webhook"
|
|
||||||
//err = removeWebhookFunction(ctx, fileId)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Printf("Function removal issue for %s-%s: %s", image, fileId, err)
|
|
||||||
// if strings.Contains(err.Error(), "does not exist") {
|
|
||||||
// resp.WriteHeader(200)
|
|
||||||
// resp.Write([]byte(`{"success": true, "reason": "Stopped webhook"}`))
|
|
||||||
|
|
||||||
// } else {
|
|
||||||
// resp.WriteHeader(401)
|
|
||||||
// resp.Write([]byte(`{"success": false, "reason": "Couldn't stop webhook, please try again later"}`))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
|
|
||||||
log.Printf("Successfully deleted webhook %s", fileId)
|
|
||||||
resp.WriteHeader(200)
|
|
||||||
resp.Write([]byte(`{"success": true, "reason": "Stopped webhook"}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeWebhookFunction(ctx context.Context, hookid string) error {
|
func removeWebhookFunction(ctx context.Context, hookid string) error {
|
||||||
service, err := cloudfunctions.NewService(ctx)
|
service, err := cloudfunctions.NewService(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -5794,7 +5470,7 @@ func handleStartHook(resp http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
environmentVariables := map[string]string{
|
environmentVariables := map[string]string{
|
||||||
"FUNCTION_APIKEY": user.ApiKey,
|
"FUNCTION_APIKEY": user.ApiKey,
|
||||||
"CALLBACKURL": "https://shuffler.io",
|
"CALLBACKURL": syncUrl,
|
||||||
"HOOKID": fileId,
|
"HOOKID": fileId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5874,7 +5550,7 @@ func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
startNode := trigger.ID
|
startNode := trigger.ID
|
||||||
if strings.Contains(triggerType, "email") {
|
if strings.Contains(triggerType, "email") {
|
||||||
action := CloudSyncJob{
|
action := shuffle.CloudSyncJob{
|
||||||
Type: "user_input",
|
Type: "user_input",
|
||||||
Action: "send_email",
|
Action: "send_email",
|
||||||
OrgId: organizationId,
|
OrgId: organizationId,
|
||||||
@@ -5900,7 +5576,7 @@ func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId
|
|||||||
log.Printf("Should send email to %s during execution.", email)
|
log.Printf("Should send email to %s during execution.", email)
|
||||||
}
|
}
|
||||||
if strings.Contains(triggerType, "sms") {
|
if strings.Contains(triggerType, "sms") {
|
||||||
action := CloudSyncJob{
|
action := shuffle.CloudSyncJob{
|
||||||
Type: "user_input",
|
Type: "user_input",
|
||||||
Action: "send_sms",
|
Action: "send_sms",
|
||||||
OrgId: organizationId,
|
OrgId: organizationId,
|
||||||
|
|||||||
@@ -191,6 +191,45 @@ const Admin = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStopOrgSync = (org_id) => {
|
||||||
|
if (org_id === undefined || org_id === null) {
|
||||||
|
alert.error("Couldn't get org "+org_id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {}
|
||||||
|
|
||||||
|
const url = globalUrl + '/api/v1/orgs/' + org_id + "/stop_sync";
|
||||||
|
fetch(url, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: 'include',
|
||||||
|
crossDomain: true,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log("Cloud sync success?")
|
||||||
|
alert.success("Successfully stopped cloud sync")
|
||||||
|
} else {
|
||||||
|
console.log("Cloud sync fail?")
|
||||||
|
alert.error("Failed stopping sync. Try again, and contact support if this persists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
handleGetOrg(org_id)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert.error("Err: " + error.toString())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const enableCloudSync = (apikey, organization, disableSync) => {
|
const enableCloudSync = (apikey, organization, disableSync) => {
|
||||||
setOrgSyncResponse("")
|
setOrgSyncResponse("")
|
||||||
|
|
||||||
@@ -1475,27 +1514,41 @@ const Admin = (props) => {
|
|||||||
<Typography style={{whiteSpace: "nowrap", marginTop: 25, marginRight: 10}}>
|
<Typography style={{whiteSpace: "nowrap", marginTop: 25, marginRight: 10}}>
|
||||||
Your Apikey
|
Your Apikey
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<div style={{display: "flex"}}>
|
||||||
color="primary"
|
<TextField
|
||||||
style={{backgroundColor: theme.palette.inputColor, }}
|
color="primary"
|
||||||
InputProps={{
|
style={{backgroundColor: theme.palette.inputColor, }}
|
||||||
style: {
|
InputProps={{
|
||||||
height: "50px",
|
style: {
|
||||||
color: "white",
|
height: "50px",
|
||||||
fontSize: "1em",
|
color: "white",
|
||||||
},
|
fontSize: "1em",
|
||||||
}}
|
},
|
||||||
required
|
}}
|
||||||
fullWidth={true}
|
required
|
||||||
disabled={true}
|
fullWidth={true}
|
||||||
autoComplete="cloud apikey"
|
disabled={true}
|
||||||
id="apikey_field"
|
autoComplete="cloud apikey"
|
||||||
margin="normal"
|
id="apikey_field"
|
||||||
placeholder="Cloud Apikey"
|
margin="normal"
|
||||||
variant="outlined"
|
placeholder="Cloud Apikey"
|
||||||
defaultValue={userSettings.apikey}
|
variant="outlined"
|
||||||
/>
|
defaultValue={userSettings.apikey}
|
||||||
</div>
|
/>
|
||||||
|
{selectedOrganization.cloud_sync_active ?
|
||||||
|
<Button
|
||||||
|
style={{ width: 150, height: 50, marginLeft: 10, marginTop: 17, }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
handleStopOrgSync(selectedOrganization.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Stop Sync
|
||||||
|
</Button>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -785,6 +785,10 @@ const AngularWorkflow = (props) => {
|
|||||||
console.log(responseJson)
|
console.log(responseJson)
|
||||||
alert.error("Failed to save: "+responseJson.reason)
|
alert.error("Failed to save: "+responseJson.reason)
|
||||||
} else {
|
} else {
|
||||||
|
if (responseJson.new_id !== undefined && responseJson.new_id !== null) {
|
||||||
|
window.location.pathname = "/workflows/"+responseJson.new_id
|
||||||
|
}
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
if (responseJson.errors !== undefined) {
|
if (responseJson.errors !== undefined) {
|
||||||
//console.log(responseJson)
|
//console.log(responseJson)
|
||||||
@@ -800,6 +804,7 @@ const AngularWorkflow = (props) => {
|
|||||||
|
|
||||||
setWorkflow(workflow)
|
setWorkflow(workflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
//alert.success("Successfully saved workflow")
|
//alert.success("Successfully saved workflow")
|
||||||
setSavingState(1)
|
setSavingState(1)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -833,6 +838,11 @@ const AngularWorkflow = (props) => {
|
|||||||
console.log("FIXME: Might have forgotten to save before executing.")
|
console.log("FIXME: Might have forgotten to save before executing.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workflow.public) {
|
||||||
|
alert.info("Save it to get a new version")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var returncheck = monitorUpdates()
|
var returncheck = monitorUpdates()
|
||||||
if (!returncheck) {
|
if (!returncheck) {
|
||||||
alert.error("No startnode set.")
|
alert.error("No startnode set.")
|
||||||
@@ -1041,7 +1051,7 @@ const AngularWorkflow = (props) => {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
setAuthLoaded(true)
|
setAuthLoaded(true)
|
||||||
alert.error("Auth loading error: ", error.toString())
|
alert.error("Auth loading error: "+error.toString())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1110,6 +1120,11 @@ const AngularWorkflow = (props) => {
|
|||||||
responseJson.errors = []
|
responseJson.errors = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (responseJson.public) {
|
||||||
|
alert.info("This workflow is public. You'll have to save it to make it your own!")
|
||||||
|
setLastSaved(false)
|
||||||
|
}
|
||||||
|
|
||||||
setWorkflow(responseJson)
|
setWorkflow(responseJson)
|
||||||
setWorkflowDone(true)
|
setWorkflowDone(true)
|
||||||
})
|
})
|
||||||
@@ -6256,6 +6271,14 @@ const AngularWorkflow = (props) => {
|
|||||||
<h2 style={{margin: 0,}}>
|
<h2 style={{margin: 0,}}>
|
||||||
{workflow.name}
|
{workflow.name}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
{workflow.public ?
|
||||||
|
<h2 style={{margin: 0,}}>
|
||||||
|
Public Workflow PREVIEW
|
||||||
|
</h2>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -6380,7 +6403,7 @@ const AngularWorkflow = (props) => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
<Tooltip color="secondary" title="Workflow settings" placement="top-start">
|
<Tooltip color="secondary" title="Workflow settings" placement="top-start">
|
||||||
<span>
|
<span>
|
||||||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={(event) => {
|
<Button disabled={workflow.public} color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={(event) => {
|
||||||
setShowShuffleMenu(!showShuffleMenu)
|
setShowShuffleMenu(!showShuffleMenu)
|
||||||
setNewAnchor(event.currentTarget)
|
setNewAnchor(event.currentTarget)
|
||||||
}}>
|
}}>
|
||||||
@@ -6407,9 +6430,9 @@ const AngularWorkflow = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
:
|
:
|
||||||
<Tooltip color="primary" title="Test execution" placement="top">
|
<Tooltip color="primary" title="Test Execution" placement="top">
|
||||||
<span>
|
<span>
|
||||||
<Button disabled={executionRequestStarted || !workflow.isValid} style={{height: boxSize, width: boxSize}} color="primary" variant="contained" onClick={() => {
|
<Button disabled={workflow.public || executionRequestStarted || !workflow.isValid} style={{height: boxSize, width: boxSize}} color="primary" variant="contained" onClick={() => {
|
||||||
executeWorkflow(executionText, workflow.start)
|
executeWorkflow(executionText, workflow.start)
|
||||||
}}>
|
}}>
|
||||||
<PlayArrowIcon style={{ fontSize: 60}} />
|
<PlayArrowIcon style={{ fontSize: 60}} />
|
||||||
@@ -6425,6 +6448,7 @@ const AngularWorkflow = (props) => {
|
|||||||
<TextField
|
<TextField
|
||||||
id="execution_argument_input_field"
|
id="execution_argument_input_field"
|
||||||
style={textFieldStyle}
|
style={textFieldStyle}
|
||||||
|
disabled={workflow.public}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
style: innerTextfieldStyle,
|
style: innerTextfieldStyle,
|
||||||
}}
|
}}
|
||||||
@@ -6461,7 +6485,7 @@ const AngularWorkflow = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip color="secondary" title="Show executions" placement="top-start">
|
<Tooltip color="secondary" title="Show executions" placement="top-start">
|
||||||
<span>
|
<span>
|
||||||
<Button color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
<Button disabled={workflow.public} color="primary" style={{height: 50, marginLeft: 10, }} variant="outlined" onClick={() => {
|
||||||
setExecutionModalOpen(true)
|
setExecutionModalOpen(true)
|
||||||
getWorkflowExecution(props.match.params.key, "")
|
getWorkflowExecution(props.match.params.key, "")
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ const Apps = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.sharing) {
|
if (data.sharing) {
|
||||||
setSharingConfiguration("everyone")
|
setSharingConfiguration(isCloud ? "public" : "everyone")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
@@ -602,7 +602,7 @@ const Apps = (props) => {
|
|||||||
|
|
||||||
const userRoles = [
|
const userRoles = [
|
||||||
"you",
|
"you",
|
||||||
"everyone",
|
isCloud ? "public" : "everyone",
|
||||||
]
|
]
|
||||||
|
|
||||||
//fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"),
|
//fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"),
|
||||||
@@ -666,7 +666,7 @@ const Apps = (props) => {
|
|||||||
|
|
||||||
if (event.target.value === "you") {
|
if (event.target.value === "you") {
|
||||||
updateAppField(selectedApp.id, "sharing", false)
|
updateAppField(selectedApp.id, "sharing", false)
|
||||||
} else if (event.target.value === "everyone") {
|
} else if (event.target.value === "everyone" || event.target.value === "public") {
|
||||||
updateAppField(selectedApp.id, "sharing", true)
|
updateAppField(selectedApp.id, "sharing", true)
|
||||||
} else {
|
} else {
|
||||||
console.log("Can't handle value for sharing: ", event.target.value)
|
console.log("Can't handle value for sharing: ", event.target.value)
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const Workflows = (props) => {
|
|||||||
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
|
||||||
const [editingWorkflow, setEditingWorkflow] = React.useState({})
|
const [editingWorkflow, setEditingWorkflow] = React.useState({})
|
||||||
const [executionLoading, setExecutionLoading] = React.useState(false)
|
const [executionLoading, setExecutionLoading] = React.useState(false)
|
||||||
|
const [importLoading, setImportLoading] = React.useState(false)
|
||||||
const [isDropzone, setIsDropzone] = React.useState(false);
|
const [isDropzone, setIsDropzone] = React.useState(false);
|
||||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
|
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
|
||||||
|
|
||||||
@@ -390,10 +391,7 @@ const Workflows = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportWorkflow = (data) => {
|
const sanitizeWorkflow = (data) => {
|
||||||
console.log("export")
|
|
||||||
let exportFileDefaultName = data.name+'.json';
|
|
||||||
|
|
||||||
data["owner"] = ""
|
data["owner"] = ""
|
||||||
console.log(data)
|
console.log(data)
|
||||||
if (data.triggers !== null && data.triggers !== undefined) {
|
if (data.triggers !== null && data.triggers !== undefined) {
|
||||||
@@ -413,16 +411,17 @@ const Workflows = (props) => {
|
|||||||
for (var branchkey in data.branches) {
|
for (var branchkey in data.branches) {
|
||||||
const branch = data.branches[branchkey]
|
const branch = data.branches[branchkey]
|
||||||
if (branch.source_id === trigger.id) {
|
if (branch.source_id === trigger.id) {
|
||||||
console.log("CHANGING SOURCE ID")
|
//console.log("CHANGING SOURCE ID")
|
||||||
branch.source_id = newId
|
branch.source_id = newId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (branch.destination_id === trigger.id) {
|
if (branch.destination_id === trigger.id) {
|
||||||
console.log("CHANGING DESTINATION ID")
|
//console.log("CHANGING DESTINATION ID")
|
||||||
branch.destination_id = newId
|
branch.destination_id = newId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trigger.environment = isCloud ? "cloud" : "Shuffle"
|
||||||
trigger.id = newId
|
trigger.id = newId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,6 +457,7 @@ const Workflows = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.actions[key].environment = isCloud ? "cloud" : "Shuffle"
|
||||||
data.actions[key].id = newId
|
data.actions[key].id = newId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,9 +482,16 @@ const Workflows = (props) => {
|
|||||||
// These are backwards.. True = saved before. Very confuse.
|
// These are backwards.. True = saved before. Very confuse.
|
||||||
data["previously_saved"] = false
|
data["previously_saved"] = false
|
||||||
data["first_save"] = false
|
data["first_save"] = false
|
||||||
data.execution_org = {"id": ""}
|
|
||||||
console.log(data)
|
console.log(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportWorkflow = (data) => {
|
||||||
|
console.log("export")
|
||||||
|
let exportFileDefaultName = data.name+'.json';
|
||||||
|
data = sanitizeWorkflow(data)
|
||||||
|
|
||||||
let dataStr = JSON.stringify(data)
|
let dataStr = JSON.stringify(data)
|
||||||
let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||||
let linkElement = document.createElement('a');
|
let linkElement = document.createElement('a');
|
||||||
@@ -493,6 +500,44 @@ const Workflows = (props) => {
|
|||||||
linkElement.click();
|
linkElement.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publishWorkflow = (data) => {
|
||||||
|
data = JSON.parse(JSON.stringify(data))
|
||||||
|
data = sanitizeWorkflow(data)
|
||||||
|
alert.info("Sanitizing and publishing "+data.name)
|
||||||
|
|
||||||
|
const url = isCloud ? globalUrl : "https://shuffler.io"
|
||||||
|
|
||||||
|
// This ALWAYS talks to Shuffle cloud
|
||||||
|
fetch(url+"/api/v1/workflows/"+data.id+"/publish", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for workflow publish :O!")
|
||||||
|
} else {
|
||||||
|
alert.success("Successfully published workflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
if (responseJson.reason !== undefined) {
|
||||||
|
alert.error("Failed publishing: ", responseJson.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableWorkflows()
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert.error(error.toString())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const copyWorkflow = (data) => {
|
const copyWorkflow = (data) => {
|
||||||
data = JSON.parse(JSON.stringify(data))
|
data = JSON.parse(JSON.stringify(data))
|
||||||
alert.success("Copying workflow "+data.name)
|
alert.success("Copying workflow "+data.name)
|
||||||
@@ -564,11 +609,16 @@ const Workflows = (props) => {
|
|||||||
boxWidth = "4px"
|
boxWidth = "4px"
|
||||||
}
|
}
|
||||||
|
|
||||||
var boxColor = "orange"
|
var boxColor = "#f85a3e"
|
||||||
if (data.is_valid) {
|
if (data.is_valid) {
|
||||||
boxColor = "green"
|
boxColor = "green"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//console.log(data)
|
||||||
|
if (!data.previously_saved) {
|
||||||
|
boxColor = "#f85a3e"
|
||||||
|
}
|
||||||
|
|
||||||
const menuClick = (event) => {
|
const menuClick = (event) => {
|
||||||
setOpen(!open)
|
setOpen(!open)
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -640,10 +690,16 @@ const Workflows = (props) => {
|
|||||||
setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags)))
|
setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags)))
|
||||||
}
|
}
|
||||||
}} key={"change"}>{"Change details"}</MenuItem>
|
}} key={"change"}>{"Change details"}</MenuItem>
|
||||||
|
{isCloud ?
|
||||||
|
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||||||
|
console.log("Should publish", data)
|
||||||
|
publishWorkflow(data)
|
||||||
|
}} key={"publish"}>{"Publish Workflow"}</MenuItem>
|
||||||
|
: null}
|
||||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||||||
copyWorkflow(data)
|
copyWorkflow(data)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}} key={"copy"}>{"Copy"}</MenuItem>
|
}} key={"duplicate"}>{"Duplicate Workflow"}</MenuItem>
|
||||||
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
|
||||||
exportWorkflow(data)
|
exportWorkflow(data)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@@ -1005,7 +1061,7 @@ const Workflows = (props) => {
|
|||||||
if (editingWorkflow.id !== undefined) {
|
if (editingWorkflow.id !== undefined) {
|
||||||
console.log("Building original workflow")
|
console.log("Building original workflow")
|
||||||
method = "PUT"
|
method = "PUT"
|
||||||
extraData = "/"+editingWorkflow.id
|
extraData = "/"+editingWorkflow.id+"?skip_save=true"
|
||||||
workflowdata = editingWorkflow
|
workflowdata = editingWorkflow
|
||||||
|
|
||||||
console.log("REMOVING OWNER")
|
console.log("REMOVING OWNER")
|
||||||
@@ -1043,6 +1099,7 @@ const Workflows = (props) => {
|
|||||||
} else if (!redirect) {
|
} else if (!redirect) {
|
||||||
// Update :)
|
// Update :)
|
||||||
getAvailableWorkflows()
|
getAvailableWorkflows()
|
||||||
|
setImportLoading(false)
|
||||||
} else {
|
} else {
|
||||||
alert.info("Successfully changed basic info for workflow")
|
alert.info("Successfully changed basic info for workflow")
|
||||||
}
|
}
|
||||||
@@ -1051,12 +1108,15 @@ const Workflows = (props) => {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
alert.error(error.toString())
|
alert.error(error.toString())
|
||||||
|
setImportLoading(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const importFiles = (event) => {
|
const importFiles = (event) => {
|
||||||
console.log("Importing!")
|
console.log("Importing!")
|
||||||
|
|
||||||
|
setImportLoading(true)
|
||||||
const file = event.target.value
|
const file = event.target.value
|
||||||
if (event.target.files.length > 0) {
|
if (event.target.files.length > 0) {
|
||||||
for (var key in event.target.files) {
|
for (var key in event.target.files) {
|
||||||
@@ -1077,6 +1137,7 @@ const Workflows = (props) => {
|
|||||||
data = JSON.parse(reader.result)
|
data = JSON.parse(reader.result)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert.error("Invalid JSON: "+e)
|
alert.error("Invalid JSON: "+e)
|
||||||
|
setImportLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1242,9 +1303,15 @@ const Workflows = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
: null}
|
: null}
|
||||||
<Tooltip color="primary" title={"Import workflows"} placement="top">
|
<Tooltip color="primary" title={"Import workflows"} placement="top">
|
||||||
<Button color="primary" style={{}} variant="text" onClick={() => upload.click()}>
|
{importLoading ?
|
||||||
<PublishIcon />
|
<Button color="primary" style={{}} variant="text" onClick={() => {}}>
|
||||||
</Button>
|
<CircularProgress style={{maxHeight: 15, maxWidth: 15,}} />
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<Button color="primary" style={{}} variant="text" onClick={() => upload.click()}>
|
||||||
|
<PublishIcon />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<input hidden type="file" multiple="multiple" ref={(ref) => upload = ref} onChange={importFiles} />
|
<input hidden type="file" multiple="multiple" ref={(ref) => upload = ref} onChange={importFiles} />
|
||||||
{workflows.length > 0 ?
|
{workflows.length > 0 ?
|
||||||
|
|||||||
Reference in New Issue
Block a user