Graphical overhaul.
Remove all legacy pixeldrain pages, handlers and assets. Remove old theming engine. Make logo responsive to the accent colour. Lots of other stuff.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -10,37 +9,30 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fornaxian.tech/log"
|
||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||
"fornaxian.tech/util"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
blackfriday "github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
APIURLExternal string `toml:"api_url_external"`
|
||||
APIURLInternal string `toml:"api_url_internal"`
|
||||
APISocketPath string `toml:"api_socket_path"`
|
||||
SessionCookieDomain string `toml:"session_cookie_domain"`
|
||||
ResourceDir string `toml:"resource_dir"`
|
||||
DebugMode bool `toml:"debug_mode"`
|
||||
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
||||
MaintenanceMode bool `toml:"maintenance_mode"`
|
||||
APIURLExternal string `toml:"api_url_external"`
|
||||
APIURLInternal string `toml:"api_url_internal"`
|
||||
APISocketPath string `toml:"api_socket_path"`
|
||||
ResourceDir string `toml:"resource_dir"`
|
||||
DebugMode bool `toml:"debug_mode"`
|
||||
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
||||
}
|
||||
|
||||
// WebController controls how requests are handled and makes sure they have
|
||||
// proper context when running
|
||||
type WebController struct {
|
||||
templates *TemplateManager
|
||||
config Config
|
||||
tpl *template.Template
|
||||
config Config
|
||||
|
||||
// Server hostname, displayed in the footer of every web page
|
||||
hostname string
|
||||
|
||||
httpClient *http.Client
|
||||
|
||||
// API client to use for all requests. If the user is authenticated you
|
||||
// should call Login() on this object. Calling Login will create a copy and
|
||||
// not alter the original PixelAPI, but it will use the same HTTP Transport
|
||||
@@ -48,49 +40,44 @@ type WebController struct {
|
||||
}
|
||||
|
||||
// New initializes a new WebController by registering all the request handlers
|
||||
// and parsing all templates in the resource directory
|
||||
func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
||||
var err error
|
||||
wc = &WebController{
|
||||
config: conf,
|
||||
httpClient: &http.Client{Timeout: time.Minute * 10},
|
||||
api: pixelapi.New(conf.APIURLInternal),
|
||||
// and parsing the page template
|
||||
func New(r *httprouter.Router, conf Config) (wc *WebController) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not get hostname: %s", err))
|
||||
}
|
||||
|
||||
wc = &WebController{
|
||||
config: conf,
|
||||
hostname: hostname,
|
||||
api: pixelapi.New(conf.APIURLInternal),
|
||||
}
|
||||
if conf.APISocketPath != "" {
|
||||
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
|
||||
}
|
||||
|
||||
wc.templates = NewTemplateManager(conf.ResourceDir, conf.APIURLExternal, conf.DebugMode)
|
||||
wc.templates.ParseTemplates(false)
|
||||
|
||||
if wc.hostname, err = os.Hostname(); err != nil {
|
||||
panic(fmt.Errorf("could not get hostname: %s", err))
|
||||
}
|
||||
wc.tpl = wc.parseTemplate()
|
||||
|
||||
// Serve static files
|
||||
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
|
||||
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Cache resources for a year
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
// Cache resources for a year. Except the Svelte entrypoint, it keeps its
|
||||
// name when it changes. The chunks it imports have a hash in their name
|
||||
if p.ByName("filepath") == "/svelte/wrap.js" {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
r.URL.Path = p.ByName("filepath")
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
r.HEAD(prefix+"/res/*filepath", resourceHandler)
|
||||
r.OPTIONS(prefix+"/res/*filepath", resourceHandler)
|
||||
r.GET(prefix+"/res/*filepath", resourceHandler)
|
||||
r.HEAD("/res/*filepath", resourceHandler)
|
||||
r.OPTIONS("/res/*filepath", resourceHandler)
|
||||
r.GET("/res/*filepath", resourceHandler)
|
||||
|
||||
// Static assets
|
||||
r.GET(prefix+"/favicon.ico" /* */, wc.serveFile("/favicon.ico"))
|
||||
r.GET(prefix+"/robots.txt" /* */, wc.serveFile("/robots.txt"))
|
||||
|
||||
if conf.MaintenanceMode {
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(r))
|
||||
})
|
||||
return wc
|
||||
}
|
||||
// Browsers and crawlers look for these in the root
|
||||
var rootFile = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fs.ServeHTTP(w, r) }
|
||||
r.GET("/favicon.ico", rootFile)
|
||||
r.GET("/robots.txt", rootFile)
|
||||
|
||||
if conf.ProxyAPIRequests {
|
||||
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
|
||||
@@ -100,50 +87,30 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
||||
|
||||
log.Info("Starting API proxy to %s", remoteURL)
|
||||
var prox = httputil.NewSingleHostReverseProxy(remoteURL)
|
||||
prox.Transport = wc.httpClient.Transport
|
||||
|
||||
var proxyHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
log.Info("Proxying request to %s", r.URL)
|
||||
r.Host = remoteURL.Host
|
||||
r.Header.Set("Origin", remoteURL.String())
|
||||
prox.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
r.Handle("OPTIONS", "/api/*p", proxyHandler)
|
||||
r.Handle("POST", "/api/*p", proxyHandler)
|
||||
r.Handle("GET", "/api/*p", proxyHandler)
|
||||
r.Handle("PUT", "/api/*p", proxyHandler)
|
||||
r.Handle("PATCH", "/api/*p", proxyHandler)
|
||||
r.Handle("DELETE", "/api/*p", proxyHandler)
|
||||
}
|
||||
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wc.serveTemplate("wrap")(w, r, nil)
|
||||
})
|
||||
|
||||
// Request method shorthands. These help keep the array of handlers aligned
|
||||
const PST, GET = "POST", "GET"
|
||||
|
||||
// Loop over the handlers and register all of them in the router
|
||||
for _, h := range []struct {
|
||||
method string // HTTP request method this API handler uses
|
||||
path string // The URL path this API will be registered on
|
||||
handler httprouter.Handle // The function to run when this API is called
|
||||
}{
|
||||
{GET, "api" /* */, wc.serveMarkdown("api.md")},
|
||||
{GET, "d/*path" /* */, wc.serveDirectory},
|
||||
{GET, "widgets" /* */, wc.serveTemplate("widgets")},
|
||||
{GET, "status" /* */, wc.serveTemplate("status")},
|
||||
{GET, "theme.css", wc.themeHandler},
|
||||
} {
|
||||
r.Handle(h.method, prefix+"/"+h.path, middleware(h.handler))
|
||||
|
||||
// Also support HEAD requests
|
||||
if h.method == GET {
|
||||
r.HEAD(prefix+"/"+h.path, middleware(h.handler))
|
||||
for _, method := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"} {
|
||||
r.Handle(method, "/api/*p", proxyHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// Every other path is a page of the app, which shows its own not found
|
||||
// message for paths it doesn't know
|
||||
var page = middleware(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
wc.renderPage(w, r, wc.newTemplateData(r))
|
||||
})
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { page(w, r, nil) })
|
||||
|
||||
// Filesystem pages embed the requested node, for link previews
|
||||
var directory = middleware(wc.serveDirectory)
|
||||
r.GET("/d/*path", directory)
|
||||
r.HEAD("/d/*path", directory)
|
||||
|
||||
return wc
|
||||
}
|
||||
|
||||
@@ -165,96 +132,6 @@ func middleware(handle httprouter.Handle) httprouter.Handle {
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) serveTemplate(tpl string) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
// if opts.NoEmbed {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
// }
|
||||
|
||||
err := wc.templates.Run(w, r, tpl, wc.newTemplateData(r))
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template '%s': %s", tpl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) serveMarkdown(tpl string) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
var err error
|
||||
// if opts.NoEmbed {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
// }
|
||||
|
||||
var tpld = wc.newTemplateData(r)
|
||||
|
||||
// Execute the raw markdown template and save the result in a buffer
|
||||
var tplBuf bytes.Buffer
|
||||
err = wc.templates.Run(&tplBuf, r, tpl, tpld)
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template '%s': %s", tpl, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the markdown document and save the resulting HTML in a buffer
|
||||
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||||
Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
|
||||
})
|
||||
|
||||
// We parse the markdown document, walk through the nodes. Extract the
|
||||
// title of the document, and the rest of the nodes are rendered like
|
||||
// normal
|
||||
var mdBuf bytes.Buffer
|
||||
|
||||
blackfriday.New(
|
||||
blackfriday.WithRenderer(renderer),
|
||||
blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
|
||||
).Parse(
|
||||
tplBuf.Bytes(),
|
||||
).Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
// Capture the title of the document so we can put it at the top of
|
||||
// the template and in the metadata. When entering a h1 node the
|
||||
// first child will be the title of the document. Save that value
|
||||
if node.Type == blackfriday.Heading && node.HeadingData.Level == 1 {
|
||||
tpld.Title = string(node.FirstChild.Literal)
|
||||
return blackfriday.SkipChildren
|
||||
}
|
||||
|
||||
// If this text node contains solely the text "[TOC]" then we render
|
||||
// the table of contents
|
||||
if node.Type == blackfriday.Text && bytes.Equal(node.Literal, []byte("[TOC]")) {
|
||||
// Find the document node and render its TOC
|
||||
for parent := node.Parent; ; parent = parent.Parent {
|
||||
if parent.Type == blackfriday.Document {
|
||||
renderer.RenderHeader(&mdBuf, parent)
|
||||
return blackfriday.SkipChildren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderer.RenderNode(&mdBuf, node, entering)
|
||||
})
|
||||
|
||||
// Pass the buffer's parsed contents to the wrapper template
|
||||
tpld.Other = template.HTML(mdBuf.Bytes())
|
||||
|
||||
// Execute the wrapper template
|
||||
err = wc.templates.Run(w, r, "markdown_wrapper", tpld)
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template '%s': %s", tpl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) serveFile(path string) httprouter.Handle {
|
||||
return func(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
p httprouter.Params,
|
||||
) {
|
||||
http.ServeFile(w, r, wc.config.ResourceDir+"/static"+path)
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
|
||||
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
|
||||
if len(cookie.Value) == 36 {
|
||||
|
||||
Reference in New Issue
Block a user