Serve the frontend's config.js with the API password
The bundled frontend gets a token with API_PASSWORD from config.js. The server now generates config.js from its own config, so setting API_PASSWORD on the server container is enough.
This commit is contained in:
1 parent
6da96f79e7
commit
fc7d787014
3 files changed
+29
-2
No files matched your search
@@ -18,8 +18,6 @@ RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache
|
|||||||
|
|
||||||
# Stage 2: Frontend static files (the same for every platform)
|
# Stage 2: Frontend static files (the same for every platform)
|
||||||
FROM --platform=$BUILDPLATFORM ${FRONTEND_IMAGE} AS frontend
|
FROM --platform=$BUILDPLATFORM ${FRONTEND_IMAGE} AS frontend
|
||||||
# Empty API_HOSTNAME makes the frontend use relative paths
|
|
||||||
RUN echo "window.__RUNTIME_CONFIG__ = { API_HOSTNAME: '' };" > /usr/share/nginx/html/config.js
|
|
||||||
|
|
||||||
# Stage 3: Final image. No RUN steps here, so no emulation is needed for other architectures.
|
# Stage 3: Final image. No RUN steps here, so no emulation is needed for other architectures.
|
||||||
FROM alpine:3.22
|
FROM alpine:3.22
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"music-server/cmd/web"
|
"music-server/cmd/web"
|
||||||
@@ -66,6 +67,7 @@ func (s *Server) routes() (*echo.Echo, apis) {
|
|||||||
e.GET("/assets/*", echo.WrapHandler(http.FileServer(http.FS(web.Assets))))
|
e.GET("/assets/*", echo.WrapHandler(http.FileServer(http.FS(web.Assets))))
|
||||||
e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm())))
|
e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm())))
|
||||||
e.POST("/find", echo.WrapHandler(web.NewSearchHandler(s.search)))
|
e.POST("/find", echo.WrapHandler(web.NewSearchHandler(s.search)))
|
||||||
|
e.GET("/config.js", s.frontendConfig)
|
||||||
e.Static("/", s.cfg.FrontendPath)
|
e.Static("/", s.cfg.FrontendPath)
|
||||||
|
|
||||||
for _, r := range e.Router().Routes() {
|
for _, r := range e.Router().Routes() {
|
||||||
@@ -74,6 +76,21 @@ func (s *Server) routes() (*echo.Echo, apis) {
|
|||||||
return e, a
|
return e, a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// frontendConfig serves the runtime config of the bundled frontend. The frontend uses
|
||||||
|
// the API password to get a token, so anyone who can open the frontend can read it.
|
||||||
|
func (s *Server) frontendConfig(c *echo.Context) error {
|
||||||
|
cfg, err := json.Marshal(map[string]string{
|
||||||
|
// Empty: the frontend calls this server with relative paths.
|
||||||
|
"API_HOSTNAME": "",
|
||||||
|
"API_PASSWORD": s.cfg.APIPassword,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Response().Header().Set("Cache-Control", "no-store")
|
||||||
|
return c.Blob(http.StatusOK, "text/javascript; charset=utf-8", []byte("window.__RUNTIME_CONFIG__ = "+string(cfg)+";\n"))
|
||||||
|
}
|
||||||
|
|
||||||
func redirect(to string) echo.HandlerFunc {
|
func redirect(to string) echo.HandlerFunc {
|
||||||
return func(c *echo.Context) error {
|
return func(c *echo.Context) error {
|
||||||
return c.Redirect(http.StatusMovedPermanently, to)
|
return c.Redirect(http.StatusMovedPermanently, to)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -142,6 +143,17 @@ func TestSearchPageAndFrontend(t *testing.T) {
|
|||||||
assert.Contains(t, rec.Body.String(), "frontend")
|
assert.Contains(t, rec.Body.String(), "frontend")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFrontendConfigHasAPIPassword(t *testing.T) {
|
||||||
|
e := newTestEnv(t, func(c *config.Config) { c.APIPassword = `pa'ss"word` })
|
||||||
|
writeFile(t, filepath.Join(e.cfg.FrontendPath, "config.js"), "stale")
|
||||||
|
|
||||||
|
rec := e.do(http.MethodGet, "/config.js")
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, "text/javascript; charset=utf-8", rec.Header().Get("Content-Type"))
|
||||||
|
assert.Equal(t, "no-store", rec.Header().Get("Cache-Control"))
|
||||||
|
assert.Equal(t, `window.__RUNTIME_CONFIG__ = {"API_HOSTNAME":"","API_PASSWORD":"pa'ss\"word"};`+"\n", rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestShutdownWaitsForRequestsBeforeClosingDatabase(t *testing.T) {
|
func TestShutdownWaitsForRequestsBeforeClosingDatabase(t *testing.T) {
|
||||||
e := newTestEnv(t)
|
e := newTestEnv(t)
|
||||||
started := make(chan struct{})
|
started := make(chan struct{})
|
||||||
|
|||||||
Reference in new issue
Block a user