From a38ebb61a9ae308665ab1ebca0e8cb74e60f6ecd Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Sat, 22 Jul 2023 21:12:53 +0200 Subject: [PATCH] form to edit registered configs --- app/config_register.go | 20 +++ cmd/owl/main.go | 7 +- render/templates/views/admin.tmpl | 9 ++ render/templates/views/admin_config.tmpl | 11 ++ web/admin_handler.go | 75 ++++++++++ web/app.go | 10 ++ web/forms/form.go | 171 +++++++++++++++++++++++ 7 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 render/templates/views/admin.tmpl create mode 100644 render/templates/views/admin_config.tmpl create mode 100644 web/admin_handler.go create mode 100644 web/forms/form.go diff --git a/app/config_register.go b/app/config_register.go index cc34104..22d5f8c 100644 --- a/app/config_register.go +++ b/app/config_register.go @@ -4,6 +4,11 @@ type ConfigRegister struct { configs map[string]interface{} } +type RegisteredConfig struct { + Name string + Config interface{} +} + func NewConfigRegister() *ConfigRegister { return &ConfigRegister{configs: map[string]interface{}{}} } @@ -11,3 +16,18 @@ func NewConfigRegister() *ConfigRegister { func (r *ConfigRegister) Register(name string, config interface{}) { r.configs[name] = config } + +func (r *ConfigRegister) Configs() []RegisteredConfig { + var configs []RegisteredConfig + for name, config := range r.configs { + configs = append(configs, RegisteredConfig{ + Name: name, + Config: config, + }) + } + return configs +} + +func (r *ConfigRegister) GetConfig(name string) interface{} { + return r.configs[name] +} diff --git a/cmd/owl/main.go b/cmd/owl/main.go index 3f244c3..ae74c6e 100644 --- a/cmd/owl/main.go +++ b/cmd/owl/main.go @@ -44,7 +44,12 @@ func App(db infra.Database) *web.WebApp { binaryService := app.NewBinaryFileService(binRepo) authorService := app.NewAuthorService(authorRepo, siteConfigRepo) - return web.NewWebApp(entryService, registry, binaryService, authorService, siteConfigRepo) + configRegister := app.NewConfigRegister() + + return web.NewWebApp( + entryService, registry, binaryService, + authorService, siteConfigRepo, configRegister, + ) } diff --git a/render/templates/views/admin.tmpl b/render/templates/views/admin.tmpl new file mode 100644 index 0000000..439ea17 --- /dev/null +++ b/render/templates/views/admin.tmpl @@ -0,0 +1,9 @@ +{{define "title"}}Admin{{end}} + +{{define "main"}} + +{{end}} \ No newline at end of file diff --git a/render/templates/views/admin_config.tmpl b/render/templates/views/admin_config.tmpl new file mode 100644 index 0000000..4a9f3fa --- /dev/null +++ b/render/templates/views/admin_config.tmpl @@ -0,0 +1,11 @@ +{{define "title"}}Editor{{end}} + +{{define "main"}} + +Back +
+
+ +{{.}} + +{{end}} \ No newline at end of file diff --git a/web/admin_handler.go b/web/admin_handler.go new file mode 100644 index 0000000..eb17420 --- /dev/null +++ b/web/admin_handler.go @@ -0,0 +1,75 @@ +package web + +import ( + "owl-blogs/app" + "owl-blogs/app/repository" + "owl-blogs/render" + "owl-blogs/web/forms" + + "github.com/gofiber/fiber/v2" +) + +type adminHandler struct { + configRepo repository.ConfigRepository + configRegister *app.ConfigRegister +} + +func NewAdminHandler(configRepo repository.ConfigRepository, configRegister *app.ConfigRegister) *adminHandler { + return &adminHandler{ + configRepo: configRepo, + configRegister: configRegister, + } +} + +func (h *adminHandler) Handle(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + + siteConfig := getSiteConfig(h.configRepo) + configs := h.configRegister.Configs() + return render.RenderTemplateWithBase(c, siteConfig, "views/admin", configs) +} + +func (h *adminHandler) HandleConfigGet(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + + configName := c.Params("config") + config := h.configRegister.GetConfig(configName) + if config == nil { + return c.SendStatus(404) + } + err := h.configRepo.Get(configName, config) + if err != nil { + return err + } + siteConfig := getSiteConfig(h.configRepo) + + form := forms.NewForm(config, nil) + htmlForm, err := form.HtmlForm() + if err != nil { + return err + } + + return render.RenderTemplateWithBase(c, siteConfig, "views/admin_config", htmlForm) +} + +func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + + configName := c.Params("config") + config := h.configRegister.GetConfig(configName) + if config == nil { + return c.SendStatus(404) + } + + form := forms.NewForm(config, nil) + + newConfig, err := form.Parse(c) + if err != nil { + return err + } + + h.configRepo.Update(configName, newConfig) + + return c.Redirect("") + +} diff --git a/web/app.go b/web/app.go index 0d5cfbf..b06d845 100644 --- a/web/app.go +++ b/web/app.go @@ -29,6 +29,7 @@ func NewWebApp( binService *app.BinaryService, authorService *app.AuthorService, configRepo repository.ConfigRepository, + configRegister *app.ConfigRegister, ) *WebApp { app := fiber.New() @@ -45,6 +46,14 @@ func NewWebApp( app.Get("/auth/login", loginHandler.HandleGet) app.Post("/auth/login", loginHandler.HandlePost) + // admin + adminHandler := NewAdminHandler(configRepo, configRegister) + admin := app.Group("/admin") + admin.Use(middleware.NewAuthMiddleware(authorService).Handle) + admin.Get("/", adminHandler.Handle) + admin.Get("/config/:config/", adminHandler.HandleConfigGet) + admin.Post("/config/:config/", adminHandler.HandleConfigPost) + // Editor editor := app.Group("/editor") editor.Use(middleware.NewAuthMiddleware(authorService).Handle) @@ -92,6 +101,7 @@ func NewWebApp( // ActivityPub // activityPubServer := NewActivityPubServer(configRepo) + configRegister.Register(ACT_PUB_CONF_NAME, &ActivityPubConfig{}) // app.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger) // app.Route("/activitypub", activityPubServer.Router) diff --git a/web/forms/form.go b/web/forms/form.go new file mode 100644 index 0000000..30de998 --- /dev/null +++ b/web/forms/form.go @@ -0,0 +1,171 @@ +package forms + +import ( + "fmt" + "mime/multipart" + "owl-blogs/app" + "reflect" + "strings" +) + +type HttpFormData interface { + // FormFile returns the first file by key from a MultipartForm. + FormFile(key string) (*multipart.FileHeader, error) + // FormValue returns the first value by key from a MultipartForm. + // Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order. + // Defaults to the empty string "" if the form value doesn't exist. + // If a default value is given, it will return that value if the form value does not exist. + // Returned value is only valid within the handler. Do not store any references. + // Make copies or use the Immutable setting instead. + FormValue(key string, defaultValue ...string) string +} + +type Form struct { + data interface{} + binSvc *app.BinaryService +} + +type FormFieldParams struct { + InputType string + Widget string +} + +type FormField struct { + Name string + Value string + Params FormFieldParams +} + +func NewForm(data interface{}, binaryService *app.BinaryService) *Form { + return &Form{ + data: data, + binSvc: binaryService, + } +} + +func (s *FormFieldParams) ApplyTag(tagKey string, tagValue string) error { + switch tagKey { + case "inputType": + s.InputType = tagValue + case "widget": + s.Widget = tagValue + default: + return fmt.Errorf("unknown tag key: %v", tagKey) + } + return nil +} + +func (s *FormField) Html() string { + html := "" + html += fmt.Sprintf("\n", s.Name, s.Name) + if s.Params.InputType == "text" && s.Params.Widget == "textarea" { + html += fmt.Sprintf("\n", s.Name, s.Name, s.Value) + } else { + html += fmt.Sprintf("\n", s.Params.InputType, s.Name, s.Name, s.Value) + } + return html +} + +func FieldToFormField(field reflect.StructField, value string) (FormField, error) { + formField := FormField{ + Name: field.Name, + Value: value, + Params: FormFieldParams{}, + } + tag := field.Tag.Get("owl") + for _, param := range strings.Split(tag, " ") { + parts := strings.Split(param, "=") + if len(parts) != 2 { + continue + } + err := formField.Params.ApplyTag(parts[0], parts[1]) + if err != nil { + return FormField{}, err + } + } + return formField, nil +} + +func StructToFormFields(data interface{}) ([]FormField, error) { + dataValue := reflect.Indirect(reflect.ValueOf(data)) + dataType := reflect.TypeOf(data).Elem() + numFields := dataType.NumField() + fields := []FormField{} + for i := 0; i < numFields; i++ { + field, err := FieldToFormField(dataType.Field(i), dataValue.FieldByIndex([]int{i}).String()) + if err != nil { + return nil, err + } + fields = append(fields, field) + } + return fields, nil +} + +func (s *Form) HtmlForm() (string, error) { + fields, err := StructToFormFields(s.data) + if err != nil { + return "", err + } + + html := "
\n" + for _, field := range fields { + html += field.Html() + } + html += "\n" + html += "
\n" + + return html, nil +} + +func (s *Form) Parse(ctx HttpFormData) (interface{}, error) { + if ctx == nil { + return nil, fmt.Errorf("nil context") + } + dataVal := reflect.ValueOf(s.data) + if dataVal.Kind() != reflect.Ptr { + return nil, fmt.Errorf("meta data is not a pointer") + } + fields, err := StructToFormFields(s.data) + if err != nil { + return nil, err + } + for _, field := range fields { + fieldName := field.Name + + if field.Params.InputType == "file" { + file, err := ctx.FormFile(fieldName) + if err != nil { + return nil, err + } + fileData, err := file.Open() + if err != nil { + return nil, err + } + defer fileData.Close() + fileBytes := make([]byte, file.Size) + _, err = fileData.Read(fileBytes) + if err != nil { + return nil, err + } + + binaryFile, err := s.binSvc.Create(file.Filename, fileBytes) + if err != nil { + return nil, err + } + + metaField := dataVal.Elem().FieldByName(fieldName) + if metaField.IsValid() { + metaField.SetString(binaryFile.Id) + } + } else { + formValue := ctx.FormValue(fieldName) + metaField := dataVal.Elem().FieldByName(fieldName) + if metaField.IsValid() { + metaField.SetString(formValue) + } + } + + } + + return s.data, nil +}