Add new UI

This commit is contained in:
akp 2024-11-17 00:39:41 +00:00
parent 9d861f2180
commit ba3ff0bd5e
No known key found for this signature in database
GPG key ID: CF8D58F3DEB20755
11 changed files with 175 additions and 48 deletions

1
go.mod
View file

@ -17,4 +17,5 @@ require (
go.uber.org/zap v1.26.0 // indirect
golang.org/x/sys v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maragu.dev/gomponents v1.0.0 // indirect
)

2
go.sum
View file

@ -33,3 +33,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maragu.dev/gomponents v1.0.0 h1:eeLScjq4PqP1l+r5z/GC+xXZhLHXa6RWUWGW7gSfLh4=
maragu.dev/gomponents v1.0.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=

View file

@ -89,14 +89,14 @@ func makeErrorHandler(statusCode int, message string) http.Handler {
})
}
func (c *Core) RouteRequest(rq *http.Request) http.Handler {
func (c *Core) RouteRequest(rq *http.Request) (http.Handler, error) {
c.routeLock.RLock()
defer c.routeLock.RUnlock()
host := strings.ToLower(rq.Host)
if host == "" {
return invalidRequestHandler
return invalidRequestHandler, nil
}
c.Logger.Debug("routing request", "host", host, "path", rq.URL.Path, "foundRoutes", c.knownRoutes[host])
@ -110,23 +110,23 @@ func (c *Core) RouteRequest(rq *http.Request) http.Handler {
if h, found := c.handlerCache[v.ContentPath]; found {
c.Logger.Debug("handler cache HIT", "content", v.ContentPath)
c.handlerCacheLock.Unlock()
return h.Handler
return h.Handler, nil
}
c.Logger.Debug("handler cache MISS", "content", v.ContentPath)
fi, err := os.Stat(p)
if err != nil {
panic(err)
return nil, fmt.Errorf("stat content file: %w", err)
}
f, err := os.Open(p)
if err != nil {
panic(err)
return nil, fmt.Errorf("open content file: %w", err)
}
zr, err := zip.NewReader(f, fi.Size())
if err != nil {
panic(err)
return nil, fmt.Errorf("create ZIP reader: %w", err)
}
fs := http.FileServerFS(zr)
@ -147,9 +147,9 @@ func (c *Core) RouteRequest(rq *http.Request) http.Handler {
c.handlerCacheLock.Unlock()
return handler
return handler, nil
}
}
return notFoundHandler
return notFoundHandler, nil
}

View file

@ -1,6 +1,7 @@
package core
import (
"database/sql"
"errors"
"fmt"
"github.com/codemicro/palmatum/palmatum/internal/database"
@ -78,8 +79,10 @@ func (c *Core) DeleteSite(siteSlug string) error {
return fmt.Errorf("commit transaction: %w", err)
}
if err := os.Remove(c.getPathOnDisk(contentPath)); err != nil {
return fmt.Errorf("remove path: %w", err)
if contentPath != "" {
if err := os.Remove(c.getPathOnDisk(contentPath)); err != nil {
return fmt.Errorf("remove path: %w", err)
}
}
if err := c.BuildKnownRoutes(); err != nil {
@ -99,11 +102,24 @@ func (c *Core) UpdateSite(s *database.SiteModel) error {
var (
ErrInvalidDomain = newError("invalid domain")
ErrInvalidPath = newError("invalid path")
ErrInvalidPath = newError("invalid path (must start with /)")
ErrRouteNotUnique = newError("route is not unique")
)
func (c *Core) CreateRoute(siteSlug, domain, path string) (*database.RouteModel, error) {
tx, err := c.Database.Beginx()
if err != nil {
return nil, fmt.Errorf("begin database transaction: %w", err)
}
defer tx.Rollback()
if _, err := database.GetSite(tx, siteSlug); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrInvalidSlug
}
return nil, fmt.Errorf("get site from database: %w", err)
}
domain = strings.TrimSpace(domain)
if domain == "" {
@ -118,7 +134,7 @@ func (c *Core) CreateRoute(siteSlug, domain, path string) (*database.RouteModel,
var id int
if err := c.Database.QueryRowx("INSERT INTO routes(site, domain, path) VALUES (?, ?, ?) RETURNING id", siteSlug, domain, path).Scan(&id); err != nil {
if err := tx.QueryRowx("INSERT INTO routes(site, domain, path) VALUES (?, ?, ?) RETURNING id", siteSlug, domain, path).Scan(&id); err != nil {
var e sqlite3.Error
if errors.As(err, &e) {
if e.ExtendedCode == sqlite3.ErrConstraintForeignKey {
@ -131,6 +147,10 @@ func (c *Core) CreateRoute(siteSlug, domain, path string) (*database.RouteModel,
return nil, fmt.Errorf("call database: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("commit databse transaction: %w", err)
}
if err := c.BuildKnownRoutes(); err != nil {
return nil, fmt.Errorf("rebuild known routes: %w", err)
}

View file

@ -7,13 +7,15 @@ import (
"github.com/codemicro/palmatum/palmatum/internal/config"
"github.com/codemicro/palmatum/palmatum/internal/core"
"github.com/codemicro/palmatum/palmatum/internal/database"
"go.uber.org/fx"
"html/template"
"log/slog"
"net/http"
"strconv"
"strings"
)
func NewManagementServer(args ServerArgs) *http.Server {
func NewManagementServer(lc fx.Lifecycle, args ServerArgs) *http.Server {
mux := http.NewServeMux()
mr := managementRoutes{
logger: args.Logger,
@ -21,6 +23,10 @@ func NewManagementServer(args ServerArgs) *http.Server {
config: args.Config,
}
lc.Append(fx.Hook{
OnStart: mr.initManagementTemplates,
})
mux.HandleFunc("POST /api/site", handleErrors(args.Logger, mr.apiCreateSite))
mux.HandleFunc("POST /api/site/bundle", handleErrors(args.Logger, mr.apiUploadSiteBundle))
mux.HandleFunc("DELETE /api/site", handleErrors(args.Logger, mr.apiDeleteSite))
@ -28,6 +34,8 @@ func NewManagementServer(args ServerArgs) *http.Server {
mux.HandleFunc("DELETE /api/site/route", handleErrors(args.Logger, mr.apiDeleteRoute))
mux.HandleFunc("GET /{$}", handleErrors(args.Logger, mr.index))
mux.HandleFunc("GET /uploadSite", handleErrors(args.Logger, mr.uploadSitePartial))
mux.HandleFunc("GET /addRoute", handleErrors(args.Logger, mr.addRoutePartial))
return newServer(args, args.Config.HTTP.ManagementAddress, mux)
}
@ -36,11 +44,8 @@ type managementRoutes struct {
logger *slog.Logger
core *core.Core
config *config.Config
}
func (mr *managementRoutes) index(rw http.ResponseWriter, rq *http.Request) error {
rw.Write([]byte("hello!"))
return nil
templates *template.Template
}
func (mr *managementRoutes) apiCreateSite(rw http.ResponseWriter, rq *http.Request) error {
@ -62,6 +67,7 @@ func (mr *managementRoutes) apiCreateSite(rw http.ResponseWriter, rq *http.Reque
return fmt.Errorf("create new site: %w", err)
}
rw.Header().Set("HX-Refresh", "true")
rw.WriteHeader(http.StatusCreated)
return nil
}
@ -79,6 +85,7 @@ func (mr *managementRoutes) apiDeleteSite(rw http.ResponseWriter, rq *http.Reque
return fmt.Errorf("delete site: %w", err)
}
rw.Header().Set("HX-Refresh", "true")
rw.WriteHeader(http.StatusOK)
return nil
}
@ -128,13 +135,12 @@ func (mr *managementRoutes) apiUploadSiteBundle(rw http.ResponseWriter, rq *http
return fmt.Errorf("update site: %w", err)
}
rw.Header().Set("HX-Refresh", "true")
rw.WriteHeader(http.StatusOK)
return nil
}
func (mr *managementRoutes) apiCreateRoute(rw http.ResponseWriter, rq *http.Request) error {
// TODO: check the route in question actually exist
siteSlug := rq.FormValue("slug")
domain := rq.FormValue("domain")
path := rq.FormValue("path")
@ -151,6 +157,7 @@ func (mr *managementRoutes) apiCreateRoute(rw http.ResponseWriter, rq *http.Requ
return fmt.Errorf("create new route: %w", err)
}
rw.Header().Set("HX-Refresh", "true")
rw.WriteHeader(http.StatusCreated)
return nil
}
@ -167,6 +174,7 @@ func (mr *managementRoutes) apiDeleteRoute(rw http.ResponseWriter, rq *http.Requ
return fmt.Errorf("delete route: %w", err)
}
rw.Header().Set("HX-Refresh", "true")
rw.WriteHeader(http.StatusOK)
return nil
}

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Palmatum management portal</title>
<link rel="stylesheet" type="text/css" href="https://cdn.tedted.dev/neat/neat.css">
<script src="https://unpkg.com/htmx.org@1.9.4" defer></script>
</head>
<body>
<h1>Palmatum Management Portal</h1>
<h2>Active sites</h2>
<ul>
{{ range $name := .ActiveSites }}<li><a href="/{{ $name }}">{{ $name }}</a> <a href="" hx-post="/-/delete" hx-vals='{"siteName": "{{ $name }}"}' hx-confirm="Are you sure you want to delete '{{ $name }}'?">[x]</a></li>{{ else }}<li><i>No active sites</i></li>{{ end }}
</ul>
<h2>Upload new site bundle</h2>
<form action="/-/upload" method="post" enctype="multipart/form-data">
<label for="siteNameInput">Site name/path: </label>
<input type="text" name="siteName" id="siteNameInput">
<br>
<label for="archiveInput">Site archive: </label>
<input type="file" name="archive" id="archiveInput">
<br>
<input type="submit" value="Submit">
</form>
</body>
</html>

View file

@ -0,0 +1,49 @@
package httpsrv
import (
"context"
"embed"
"fmt"
"github.com/codemicro/palmatum/palmatum/internal/database"
"io/fs"
"net/http"
)
//go:embed templates/*
var managementTemplateSource embed.FS
func (mr *managementRoutes) initManagementTemplates(_ context.Context) error {
f, err := fs.Sub(fs.FS(managementTemplateSource), "templates")
if err != nil {
return fmt.Errorf("subset filesystem: %w", err)
}
mr.templates, err = mr.templates.ParseFS(f, "*.html")
if err != nil {
return fmt.Errorf("parse templates: %w", err)
}
return nil
}
func (mr *managementRoutes) index(rw http.ResponseWriter, rq *http.Request) error {
var templateData = struct {
Sites []*database.SiteModel
}{}
s, err := database.GetSitesWithRoutes(mr.core.Database)
if err != nil {
return fmt.Errorf("get sites with routes: %w", err)
}
templateData.Sites = s
return mr.templates.ExecuteTemplate(rw, "index.html", &templateData)
}
func (mr *managementRoutes) uploadSitePartial(rw http.ResponseWriter, rq *http.Request) error {
return mr.templates.ExecuteTemplate(rw, "uploadSite.html", rq.URL.Query().Get("slug"))
}
func (mr *managementRoutes) addRoutePartial(rw http.ResponseWriter, rq *http.Request) error {
return mr.templates.ExecuteTemplate(rw, "addRoute.html", rq.URL.Query().Get("slug"))
}

View file

@ -1,11 +1,17 @@
package httpsrv
import (
"fmt"
"net/http"
)
func NewSitesServer(args ServerArgs) *http.Server {
return newServer(args, args.Config.HTTP.SitesAddress, http.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request) {
args.Core.RouteRequest(rq).ServeHTTP(rw, rq)
return newServer(args, args.Config.HTTP.SitesAddress, handleErrors(args.Logger, func(rw http.ResponseWriter, rq *http.Request) error {
h, err := args.Core.RouteRequest(rq)
if err != nil {
return fmt.Errorf("handle sites request: %w", err)
}
h.ServeHTTP(rw, rq)
return nil
}))
}

View file

@ -0,0 +1,13 @@
<div>
<a href="/">[go back]</a>
<h2>Add route to {{ . }}</h2>
<form hx-post="/api/site/route" hx-vals='{"slug": "{{ js . }}"}'>
<label for="domainBox">Domain*: </label>
<input type="text" name="domain" id="domainBox">
<br>
<label for="pathBox">Path: </label>
<input type="text" name="path" placeholder="/" id="pathBox">
<br>
<input type="submit" value="Save">
</form>
</div>

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Palmatum management portal</title>
<link rel="stylesheet" type="text/css" href="https://cdn.tedted.dev/neat/neat.css">
<script src="https://unpkg.com/htmx.org@1.9.4" defer></script>
<script defer>
document.addEventListener("htmx:responseError", (evt) => {
alert(`${evt.detail.xhr.statusText} - ${evt.detail.xhr.responseText}`)
})
</script>
</head>
<body hx-encoding="multipart/form-data">
<h1>Palmatum Management Portal</h1>
<div id="swapBox">
<h2>Active sites</h2>
<ul>
{{ range .Sites }}
<li>{{ .Slug }}
<a hx-delete="/api/site" hx-vals='{"slug": "{{ js .Slug }}"}' hx-confirm="Are you sure you want to delete {{ js .Slug }}?" href="">[delete]</a>
<a hx-get="/uploadSite" hx-vals='{"slug": "{{ js .Slug }}"}' hx-target="#swapBox" hx-swap="outerHTML" href="">[upload]</a>
{{ if eq .ContentPath "" }}<span style="color: red; font-style: italic;">no site uploaded</span>{{ end }}
<ul>
{{ range .Routes }}
<li><a href="//{{ .Domain }}{{ .Path }}">{{ .Domain }}{{ .Path }}</a> <a hx-delete="/api/site/route" hx-vals='{"id": {{ .ID }}}' hx-confirm="Are you sure you want to delete {{ .Domain }}{{ .Path }}?" href="">[delete]</a></li>
{{ else }}
<li><span style="color: red; font-style: italic">no routes</span></li>
{{ end }}
<li><a hx-get="/addRoute" hx-vals='{"slug": "{{ js .Slug }}"}' hx-target="#swapBox" hx-swap="outerHTML" href="">[add route]</a></li>
</ul>
</li>
{{ else }}
<li><span style="color: red; font-style: italic">no sites</span></li>
{{ end }}
</ul>
<h2>Create new site</h2>
<form hx-post="/api/site">
<label for="siteNameInput">Site slug: </label>
<input type="text" name="slug" id="siteNameInput">
<input type="submit" value="Submit">
</form>
</div>
</body>
</html>

View file

@ -0,0 +1,9 @@
<div>
<a href="/">[go back]</a>
<h2>Upload to {{ . }}</h2>
<form hx-post="/api/site/bundle" hx-vals='{"slug": "{{ js . }}"}'>
<label for="siteBundleBox">Site bundle: </label>
<input type="file" name="bundle" id="siteBundleBox">
<input type="submit" value="Upload">
</form>
</div>