package engine
import (
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
"git.sr.ht/~humaid/workflow-engine/public"
"git.sr.ht/~humaid/workflow-engine/templates"
"github.com/go-macaron/bindata"
"github.com/go-macaron/binding"
"github.com/go-macaron/csrf"
"github.com/go-macaron/session"
"gopkg.in/macaron.v1"
)
func requireLogin(ctx *macaron.Context, sess session.Store, f *session.Flash) {
if sess.Get("auth") != true {
f.Error("Please login first!")
ctx.Redirect("/")
return
}
}
func contextInit(e *Engine) macaron.Handler {
return func(ctx *macaron.Context, sess session.Store, x csrf.CSRF) {
ctx.Data["csrf_token"] = x.GetToken()
ctx.Data["Engine"] = e
ctx.Data["LoggedIn"] = sess.Get("auth")
ctx.Data["SchemasModified"] = e.SchemasModified
}
}
func (e *Engine) RunWebInterface() {
m := macaron.Classic()
renderOpts := macaron.RenderOptions{
Funcs: []template.FuncMap{map[string]interface{}{
"csv": func(s []string) string {
var str strings.Builder
for _, v := range s {
if str.Len() > 0 {
str.WriteRune(',')
}
str.WriteString(v)
}
return str.String()
},
}},
}
staticOpts := macaron.StaticOptions{
Expires: func() string {
return time.Now().Add(24 * 60 * time.Minute).UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
},
}
if !e.DevMode {
renderOpts.TemplateFileSystem = bindata.Templates(bindata.Options{
Asset: templates.Asset,
AssetDir: templates.AssetDir,
AssetNames: templates.AssetNames,
Prefix: "",
})
staticOpts.FileSystem = bindata.Static(bindata.Options{
Asset: public.Asset,
AssetDir: public.AssetDir,
AssetNames: public.AssetNames,
Prefix: "",
})
}
m.Use(macaron.Renderer(renderOpts))
m.Use(session.Sessioner())
m.Use(macaron.Static("public", staticOpts))
m.Use(csrf.Csrfer())
m.Use(contextInit(e))
m.Get("/", loginHandler)
m.Post("/", csrf.Validate, postLoginHandler)
m.Group("", func() {
m.Get("/dash", dashboardHandler)
m.Get("/s/:service", serviceHandler)
m.Get("/s/:service/:channel", channelHandler)
m.Get("/s/:service/:channel/:step", stepHandler)
m.Group("/new", func() {
m.Get("/service", newServiceHandler)
m.Post("/service", binding.BindIgnErr(newServiceForm{}), postNewServiceHandler)
m.Get("/channel/:service", newChannelHandler)
m.Post("/channel/:service", binding.BindIgnErr(newChannelForm{}), postNewChannelHandler)
m.Get("/step/:service/:channel", newStepHandler)
m.Post("/step/:service/:channel", binding.BindIgnErr(newStepForm{}), postNewStepHandler)
})
m.Group("/edit", func() {
m.Get("/service/:service", editServiceHandler)
m.Post("/service/:service", binding.BindIgnErr(newServiceForm{}), postEditServiceHandler)
m.Get("/channel/:service/:channel", editChannelHandler)
m.Post("/channel/:service/:channel", binding.BindIgnErr(newChannelForm{}), postEditChannelHandler)
m.Get("/step/:service/:channel/:step", editStepHandler)
m.Post("/step/:service/:channel/:step", binding.BindIgnErr(newStepForm{}), postEditStepHandler)
})
m.Get("/commit", commitHandler)
m.Get("/errors", errorsHandler)
}, requireLogin)
log.Fatal(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", e.Options.AdminPort), m))
}
func errorsHandler(ctx *macaron.Context) {
ctx.HTML(http.StatusOK, "errors")
}
func editStepHandler(ctx *macaron.Context, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
s := e.getStep(ctx.Params("service"), ctx.Params("channel"), ctx.Params("step"))
if s == nil {
f.Error("Step not found")
ctx.Redirect("/dash")
return
}
ctx.Data["Step"] = s
ctx.HTML(http.StatusOK, "edit-step")
}
func postEditStepHandler(ctx *macaron.Context, f *session.Flash, errs binding.Errors, form newStepForm) {
if errs.Len() > 0 {
f.Error("Make sure to fill required fields")
ctx.Redirect("/edit/service/" + ctx.Params("service") + "/" + ctx.Params("channel") + "/" + ctx.Params("step"))
return
}
e := ctx.Data["Engine"].(*Engine)
s := e.getStep(ctx.Params("service"), ctx.Params("channel"), ctx.Params("step"))
if s == nil {
f.Error("Step not found")
ctx.Redirect("/dash")
return
}
s.Status = form.Status
s.Page = form.Page
s.PostTo = strings.Split(form.PostTo, ",")
s.DataIn = strings.Split(form.DataIn, ",")
s.DataOut = strings.Split(form.DataOut, ",")
ctx.Redirect("/s/" + ctx.Params("service") + "/" + ctx.Params("channel") + "/" + ctx.Params("step"))
}
func editChannelHandler(ctx *macaron.Context, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
c := e.getChannel(ctx.Params("service"), ctx.Params("channel"))
if c == nil {
f.Error("Channel not found")
ctx.Redirect("/dash")
return
}
ctx.Data["Channel"] = c
ctx.HTML(http.StatusOK, "edit-channel")
}
func postEditChannelHandler(ctx *macaron.Context, f *session.Flash, errs binding.Errors, form newChannelForm) {
if errs.Len() > 0 {
f.Error("Make sure to fill required fields")
ctx.Redirect("/edit/service/" + ctx.Params("service") + "/" + ctx.Params("channel"))
return
}
e := ctx.Data["Engine"].(*Engine)
c := e.getChannel(ctx.Params("service"), ctx.Params("channel"))
if c == nil {
f.Error("Channel not found")
ctx.Redirect("/dash")
return
}
c.ChannelDescription = form.Description
c.Initiators = strings.Split(form.Initiators, ",")
ctx.Redirect("/s/" + ctx.Params("service") + "/" + ctx.Params("channel"))
}
func editServiceHandler(ctx *macaron.Context, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
s := e.getService(ctx.Params("service"))
if s == nil {
f.Error("Service not found")
ctx.Redirect("/dash")
return
}
ctx.Data["Service"] = s
ctx.HTML(http.StatusOK, "edit-service")
}
func postEditServiceHandler(ctx *macaron.Context, f *session.Flash, errs binding.Errors, form newServiceForm) {
if errs.Len() > 0 {
f.Error("Make sure to fill required fields")
ctx.Redirect("/edit/service/" + ctx.Params("service"))
return
}
e := ctx.Data["Engine"].(*Engine)
s := e.getService(ctx.Params("service"))
if s == nil {
f.Error("Service not found")
ctx.Redirect("/dash")
return
}
s.Category = form.Category
s.Description = form.Description
ctx.Redirect("/s/" + s.ServiceID)
}
func toFileFriendly(id string) string {
id = strings.ToLower(id)
id = strings.ReplaceAll(id, " ", "-")
return id
}
func commitHandler(ctx *macaron.Context) {
e := ctx.Data["Engine"].(*Engine)
for _, s := range e.Services {
fmt.Println(s.ServiceID)
j, err := json.MarshalIndent(s, "", "\t")
if err != nil {
log.Println("Failed marshalling service: ", err)
continue
}
var fileName string
if len(s.FileName) > 0 {
fileName = s.FileName
} else {
if strings.HasPrefix(strings.ToLower(s.ServiceID), "service") {
fileName = s.ServiceID + ".json"
} else {
fileName = "service-" + s.ServiceID + ".json"
}
fileName = toFileFriendly(fileName)
}
err = ioutil.WriteFile(e.Options.WorkflowDirPath+"/"+fileName, j, 0600)
if err != nil {
log.Println("Failed saving service to file: ", err)
continue
}
for _, ch := range s.Channels {
fmt.Println(ch.ChannelID)
j, err := json.MarshalIndent(ch, "", "\t")
if err != nil {
log.Println("Failed marshalling channel: ", err)
continue
}
var fileName string
if len(ch.FileName) > 0 {
fileName = ch.FileName
} else {
fileName = toFileFriendly(ch.ChannelID) + "-" + s.ServiceID + ".json"
}
err = ioutil.WriteFile(e.Options.WorkflowDirPath+"/"+fileName, j, 0600)
if err != nil {
log.Println("Failed saving channel to file: ", err)
continue
}
}
}
e.SchemasModified = false
ctx.Redirect("/dash")
}
func newStepHandler(ctx *macaron.Context, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
ch := e.getChannel(ctx.Params("service"), ctx.Params("channel"))
if ch == nil {
f.Error("Service and channel combination not found")
ctx.Redirect("/dash")
return
}
ctx.Data["Channel"] = ch
ctx.HTML(http.StatusOK, "new-step")
}
func postNewStepHandler(ctx *macaron.Context, errs binding.Errors, form newStepForm, f *session.Flash) {
if errs.Len() > 0 {
f.Error("Make sure to fill required fields")
ctx.Redirect("/new/channel/" + ctx.Params("service"))
return
}
e := ctx.Data["Engine"].(*Engine)
ch := e.getChannel(ctx.Params("service"), ctx.Params("channel"))
if ch == nil {
f.Error("Service and channel combination not found")
ctx.Redirect("/dash")
return
}
st := WorkflowStep{
StepID: form.StepID,
Status: form.Status,
Page: form.Page,
}
if len(form.DataIn) > 0 {
st.DataIn = strings.Split(form.DataIn, ",")
}
if len(form.DataOut) > 0 {
st.DataOut = strings.Split(form.DataOut, ",")
}
if len(form.PostTo) > 0 {
st.PostTo = strings.Split(form.PostTo, ",")
}
ch.Steps = append(ch.Steps, st)
e.SchemasModified = true
ctx.Redirect("/s/" + ctx.Params("service") + "/" + ctx.Params("channel") + "/" + st.StepID)
}
func postNewChannelHandler(ctx *macaron.Context, errs binding.Errors, form newChannelForm, f *session.Flash) {
if errs.Len() > 0 {
f.Error("Make sure to fill required fields")
ctx.Redirect("/new/channel/" + ctx.Params("service"))
return
}
e := ctx.Data["Engine"].(*Engine)
var service *Service
for i, s := range e.Services {
if s.ServiceID == ctx.Params("service") {
service = &e.Services[i]
break
}
}
if service == nil {
f.Error("Unknown service")
ctx.Redirect("/dash")
return
}
c := Channel{
ChannelID: form.ChannelID,
ChannelDescription: form.Description,
}
if len(form.Initiators) > 0 {
c.Initiators = strings.Split(form.Initiators, ",")
}
service.Channels = append(service.Channels, c)
e.SchemasModified = true
ctx.Redirect("/s/" + ctx.Params("service") + "/" + c.ChannelID)
}
func newChannelHandler(ctx *macaron.Context) {
e := ctx.Data["Engine"].(*Engine)
var service *Service
for _, s := range e.Services {
if s.ServiceID == ctx.Params("service") {
service = &s
break
}
}
ctx.Data["Service"] = service
ctx.HTML(http.StatusOK, "new-channel")
}
func postNewServiceHandler(ctx *macaron.Context, errs binding.Errors, form newServiceForm, f *session.Flash) {
if errs.Len() > 0 {
f.Error("Make sure to fill required fields")
ctx.Redirect("/new/service")
return
}
e := ctx.Data["Engine"].(*Engine)
for _, s := range e.Services {
if s.ServiceID == form.ServiceID {
f.Error("Service with the same ID already exists!")
ctx.Redirect("/new/service")
return
}
}
s := Service{
ServiceID: form.ServiceID,
Description: form.Description,
Category: form.Category,
}
e.Services = append(e.Services, s)
e.SchemasModified = true
ctx.Redirect("/s/" + form.ServiceID)
}
func newServiceHandler(ctx *macaron.Context) {
ctx.HTML(http.StatusOK, "new-service")
}
func stepHandler(ctx *macaron.Context, sess session.Store, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
c := e.getChannel(ctx.Params("service"), ctx.Params("channel"))
if c == nil {
f.Error("Channel not found")
ctx.Redirect("/dash")
return
}
var step *WorkflowStep
for _, st := range c.Steps {
if st.StepID == ctx.Params("step") {
step = &st
break
}
}
if step == nil {
f.Error("Step not found")
ctx.Redirect("/dash")
return
}
ctx.Data["ServiceID"] = ctx.Params("service")
ctx.Data["ChannelID"] = ctx.Params("channel")
ctx.Data["Channel"] = c
ctx.Data["Step"] = step
ctx.Data["Rel"] = fmt.Sprintf("%s/%s/%s", ctx.Params("service"), ctx.Params("channel"), step.StepID)
ctx.HTML(http.StatusOK, "step")
}
func channelHandler(ctx *macaron.Context, sess session.Store, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
c := e.getChannel(ctx.Params("service"), ctx.Params("channel"))
if c == nil {
f.Error("Channel not found")
ctx.Redirect("/dash")
return
}
ctx.Data["ServiceID"] = ctx.Params("service")
ctx.Data["Channel"] = c
ctx.HTML(http.StatusOK, "channel")
}
func serviceHandler(ctx *macaron.Context, sess session.Store, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
var service *Service
for _, s := range e.Services {
if s.ServiceID == ctx.Params("service") {
service = &s
break
}
}
if service == nil {
f.Error("Service not found")
ctx.Redirect("/dash")
return
}
ctx.Data["Service"] = service
ctx.HTML(http.StatusOK, "service")
}
func dashboardHandler(ctx *macaron.Context, sess session.Store) {
e := ctx.Data["Engine"].(*Engine)
ctx.Data["Services"] = e.Services
ctx.HTML(http.StatusOK, "dash")
}
func loginHandler(ctx *macaron.Context, sess session.Store, f *session.Flash) {
if sess.Get("auth") == true {
ctx.Redirect("/dash")
return
}
ctx.HTML(http.StatusOK, "login")
}
func postLoginHandler(ctx *macaron.Context, sess session.Store, f *session.Flash) {
e := ctx.Data["Engine"].(*Engine)
if ctx.Query("pass") != e.Options.DashboardKey {
f.Error("Invalid passphrase!")
ctx.Redirect("/")
return
}
sess.Set("auth", true)
ctx.Redirect("/dash")
}