Compare commits
2 Commits
0a4106be9c
...
a38ebb61a9
Author | SHA1 | Date |
---|---|---|
Niko Abeler | a38ebb61a9 | |
Niko Abeler | 68e8f84220 |
|
@ -0,0 +1,33 @@
|
|||
package app
|
||||
|
||||
type ConfigRegister struct {
|
||||
configs map[string]interface{}
|
||||
}
|
||||
|
||||
type RegisteredConfig struct {
|
||||
Name string
|
||||
Config interface{}
|
||||
}
|
||||
|
||||
func NewConfigRegister() *ConfigRegister {
|
||||
return &ConfigRegister{configs: map[string]interface{}{}}
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package app
|
||||
|
||||
import "owl-blogs/domain/model"
|
||||
|
||||
type EntryCreationSubscriber interface {
|
||||
NotifyEntryCreation(entry model.Entry)
|
||||
}
|
||||
|
||||
type EntryCreationBus struct {
|
||||
subscribers []EntryCreationSubscriber
|
||||
}
|
||||
|
||||
func NewEntryCreationBus() *EntryCreationBus {
|
||||
return &EntryCreationBus{subscribers: make([]EntryCreationSubscriber, 0)}
|
||||
}
|
||||
|
||||
func (b *EntryCreationBus) Subscribe(subscriber EntryCreationSubscriber) {
|
||||
b.subscribers = append(b.subscribers, subscriber)
|
||||
}
|
||||
|
||||
func (b *EntryCreationBus) Notify(entry model.Entry) {
|
||||
for _, subscriber := range b.subscribers {
|
||||
subscriber.NotifyEntryCreation(entry)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{define "base"}}
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<html lang='en' data-theme="light">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{{define "title"}}Admin{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<ul>
|
||||
{{ range . }}
|
||||
<li><a href="/admin/config/{{.Name}}">{{.Name}}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{end}}
|
|
@ -0,0 +1,11 @@
|
|||
{{define "title"}}Editor{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
|
||||
<a href="/editor">Back</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
{{.}}
|
||||
|
||||
{{end}}
|
|
@ -0,0 +1,122 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"owl-blogs/app/repository"
|
||||
"owl-blogs/config"
|
||||
"owl-blogs/domain/model"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const ACT_PUB_CONF_NAME = "activity_pub"
|
||||
|
||||
type ActivityPubServer struct {
|
||||
configRepo repository.ConfigRepository
|
||||
}
|
||||
|
||||
type ActivityPubConfig struct {
|
||||
PreferredUsername string `owl:"inputType=text"`
|
||||
PublicKeyPem string `owl:"inputType=text widget=textarea"`
|
||||
PrivateKeyPem string `owl:"inputType=text widget=textarea"`
|
||||
}
|
||||
|
||||
type WebfingerResponse struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []ActivityPubLink `json:"links"`
|
||||
}
|
||||
|
||||
type ActivityPubLink struct {
|
||||
Rel string `json:"rel"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
}
|
||||
|
||||
type ActivityPubActor struct {
|
||||
Context []string `json:"@context"`
|
||||
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
PreferredUsername string `json:"preferredUsername"`
|
||||
Inbox string `json:"inbox"`
|
||||
Oubox string `json:"outbox"`
|
||||
Followers string `json:"followers"`
|
||||
|
||||
PublicKey ActivityPubPublicKey `json:"publicKey"`
|
||||
}
|
||||
|
||||
type ActivityPubPublicKey struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
PublicKeyPem string `json:"publicKeyPem"`
|
||||
}
|
||||
|
||||
type ActivityPubOrderedCollection struct {
|
||||
Context []string `json:"@context"`
|
||||
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
First string `json:"first"`
|
||||
Last string `json:"last"`
|
||||
}
|
||||
|
||||
func NewActivityPubServer(configRepo repository.ConfigRepository) *ActivityPubServer {
|
||||
return &ActivityPubServer{
|
||||
configRepo: configRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
||||
siteConfig := model.SiteConfig{}
|
||||
apConfig := ActivityPubConfig{}
|
||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||
|
||||
webfinger := WebfingerResponse{
|
||||
Subject: ctx.Query("resource"),
|
||||
|
||||
Links: []ActivityPubLink{
|
||||
{
|
||||
Rel: "self",
|
||||
Type: "application/activity+json",
|
||||
Href: siteConfig.FullUrl + "/activitypub/actor",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return ctx.JSON(webfinger)
|
||||
|
||||
}
|
||||
|
||||
func (s *ActivityPubServer) Router(router fiber.Router) {
|
||||
router.Get("/actor", s.HandleActor)
|
||||
}
|
||||
|
||||
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
||||
siteConfig := model.SiteConfig{}
|
||||
apConfig := ActivityPubConfig{}
|
||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||
|
||||
actor := ActivityPubActor{
|
||||
Context: []string{
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
},
|
||||
|
||||
ID: siteConfig.FullUrl + "/activitypub/actor",
|
||||
Type: "Person",
|
||||
PreferredUsername: apConfig.PreferredUsername,
|
||||
Inbox: siteConfig.FullUrl + "/activitypub/inbox",
|
||||
Oubox: siteConfig.FullUrl + "/activitypub/outbox",
|
||||
Followers: siteConfig.FullUrl + "/activitypub/followers",
|
||||
|
||||
PublicKey: ActivityPubPublicKey{
|
||||
ID: siteConfig.FullUrl + "/activitypub/actor#main-key",
|
||||
Owner: siteConfig.FullUrl + "/activitypub/actor",
|
||||
PublicKeyPem: apConfig.PublicKeyPem,
|
||||
},
|
||||
}
|
||||
|
||||
return ctx.JSON(actor)
|
||||
}
|
|
@ -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("")
|
||||
|
||||
}
|
16
web/app.go
16
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)
|
||||
|
@ -89,6 +98,13 @@ func NewWebApp(
|
|||
app.Get("/index.xml", rssHandler.Handle)
|
||||
// Posts
|
||||
app.Get("/posts/:post/", entryHandler.Handle)
|
||||
|
||||
// ActivityPub
|
||||
// activityPubServer := NewActivityPubServer(configRepo)
|
||||
configRegister.Register(ACT_PUB_CONF_NAME, &ActivityPubConfig{})
|
||||
// app.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
|
||||
// app.Route("/activitypub", activityPubServer.Router)
|
||||
|
||||
// Webmention
|
||||
// app.Post("/webmention/", userWebmentionHandler(repo))
|
||||
// Micropub
|
||||
|
|
|
@ -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("<label for=\"%v\">%v</label>\n", s.Name, s.Name)
|
||||
if s.Params.InputType == "text" && s.Params.Widget == "textarea" {
|
||||
html += fmt.Sprintf("<textarea name=\"%v\" id=\"%v\" rows=\"20\">%v</textarea>\n", s.Name, s.Name, s.Value)
|
||||
} else {
|
||||
html += fmt.Sprintf("<input type=\"%v\" name=\"%v\" id=\"%v\" value=\"%v\" />\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 := "<form method=\"POST\" enctype=\"multipart/form-data\">\n"
|
||||
for _, field := range fields {
|
||||
html += field.Html()
|
||||
}
|
||||
html += "<input type=\"submit\" value=\"Submit\" />\n"
|
||||
html += "</form>\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
|
||||
}
|
Loading…
Reference in New Issue