Refactor frontend build and name space all admin URIs behind /admin/.

- Namespace all admin UI URLs behind `/admin/*`.
  This breaks the current admin UI URLs.
- Make Vue output build assets to `frontend/dist/*` instead of
  `frontend/dist/frontend`.
- Namespace Vue static assets to `/admin/static/*`.

This commit reduces the cofusing and convoluted Vue+WebPack build URI
and static path schemes. In addition, it removes ambiguity in URLs
where non-UI URLs like `/public`, `/api`, `/webhooks` etc. were in the
same name space as UI URLs like `/campaigns`, `/lists` etc. Now all UI
URLs are behind `/admin/`, also simplifying security rules for proxies.
This commit is contained in:
Kailash Nadh 2021-09-22 20:14:31 +05:30
parent 13f16486fb
commit bb340b8785
7 changed files with 39 additions and 37 deletions

View file

@ -23,7 +23,7 @@ STATIC := config.toml.sample \
schema.sql queries.sql \
static/public:/public \
static/email-templates \
frontend/dist/frontend:/frontend \
frontend/dist:/admin \
i18n:/i18n
.PHONY: build
@ -40,14 +40,14 @@ $(FRONTEND_YARN_MODULES): frontend/package.json frontend/yarn.lock
$(BIN): $(shell find . -type f -name "*.go")
CGO_ENABLED=0 go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/*.go
# Run the backend in dev mode. The frontend assets in dev mode are loaded from disk from frontend/dist/frontend.
# Run the backend in dev mode. The frontend assets in dev mode are loaded from disk from frontend/dist.
.PHONY: run
run:
CGO_ENABLED=0 go run -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}' -X 'main.frontendDir=frontend/dist/frontend'" cmd/*.go
CGO_ENABLED=0 go run -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}' -X 'main.frontendDir=frontend/dist'" cmd/*.go
# Build the JS frontend into frontend/dist.
$(FRONTEND_DIST): $(FRONTEND_DEPS)
export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) build && mv dist/favicon.png dist/frontend/favicon.png
export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) build
touch --no-create $(FRONTEND_DIST)

View file

@ -4,6 +4,7 @@ import (
"crypto/subtle"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
@ -37,7 +38,7 @@ var (
)
// registerHandlers registers HTTP handlers.
func registerHTTPHandlers(e *echo.Echo, app *App) {
func initHTTPHandlers(e *echo.Echo, app *App) {
// Group of private handlers with BasicAuth.
var g *echo.Group
@ -48,7 +49,15 @@ func registerHTTPHandlers(e *echo.Echo, app *App) {
g = e.Group("", middleware.BasicAuth(basicAuth))
}
g.GET("/", handleIndexPage)
// Admin JS app views.
// /admin/static/* file server is registered in initHTTPServer().
g.GET("/", func(c echo.Context) error {
return c.Redirect(http.StatusPermanentRedirect, path.Join(adminRoot, ""))
})
g.GET(path.Join(adminRoot, ""), handleAdminPage)
g.GET(path.Join(adminRoot, "/*"), handleAdminPage)
// API endpoints.
g.GET("/api/health", handleHealthCheck)
g.GET("/api/config", handleGetServerConfig)
g.GET("/api/lang/:lang", handleGetI18nLang)
@ -125,21 +134,6 @@ func registerHTTPHandlers(e *echo.Echo, app *App) {
g.PUT("/api/templates/:id/default", handleTemplateSetDefault)
g.DELETE("/api/templates/:id", handleDeleteTemplate)
// Static admin views.
g.GET("/lists", handleIndexPage)
g.GET("/lists/forms", handleIndexPage)
g.GET("/subscribers", handleIndexPage)
g.GET("/subscribers/lists/:listID", handleIndexPage)
g.GET("/subscribers/import", handleIndexPage)
g.GET("/subscribers/bounces", handleIndexPage)
g.GET("/campaigns", handleIndexPage)
g.GET("/campaigns/new", handleIndexPage)
g.GET("/campaigns/media", handleIndexPage)
g.GET("/campaigns/templates", handleIndexPage)
g.GET("/campaigns/:campignID", handleIndexPage)
g.GET("/settings", handleIndexPage)
g.GET("/settings/logs", handleIndexPage)
if app.constants.BounceWebhooksEnabled {
// Private authenticated bounce endpoint.
g.POST("/webhooks/bounce", handleBounceWebhook)
@ -171,17 +165,16 @@ func registerHTTPHandlers(e *echo.Echo, app *App) {
e.GET("/health", handleHealthCheck)
}
// handleIndex is the root handler that renders the Javascript frontend.
func handleIndexPage(c echo.Context) error {
// handleAdminPage is the root handler that renders the Javascript admin frontend.
func handleAdminPage(c echo.Context) error {
app := c.Get("app").(*App)
b, err := app.fs.Read("/frontend/index.html")
b, err := app.fs.Read(path.Join(adminRoot, "/index.html"))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
c.Response().Header().Set("Content-Type", "text/html")
return c.String(http.StatusOK, string(b))
return c.HTMLBlob(http.StatusOK, b)
}
// handleHealthCheck is a healthcheck endpoint that returns a 200 response.

View file

@ -39,6 +39,9 @@ import (
const (
queryFilePath = "queries.sql"
// Root URI of the admin frontend.
adminRoot = "/admin"
)
// constants contains static, constant config values required by the app.
@ -129,9 +132,9 @@ func initFS(appDir, frontendDir, staticDir, i18nDir string) stuffbin.FileSystem
}
frontendFiles = []string{
// The app's frontend assets are accessible at /frontend/js/* during runtime.
// These paths are joined with frontendDir.
"./:/frontend",
// Admin frontend's static assets accessible at /admin/* during runtime.
// These paths are sourced from frontendDir.
"./:/admin",
}
staticFiles = []string{
@ -574,15 +577,21 @@ func initHTTPServer(app *App) *echo.Echo {
// Initialize the static file server.
fSrv := app.fs.FileServer()
// Public (subscriber) facing static files.
srv.GET("/public/*", echo.WrapHandler(fSrv))
srv.GET("/frontend/*", echo.WrapHandler(fSrv))
// Admin (frontend) facing static files.
srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
// Public (subscriber) facing media upload files.
if ko.String("upload.provider") == "filesystem" {
srv.Static(ko.String("upload.filesystem.upload_uri"),
ko.String("upload.filesystem.upload_path"))
}
// Register all HTTP handlers.
registerHTTPHandlers(srv, app)
initHTTPHandlers(srv, app)
// Start the server.
go func() {

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
<link rel="icon" href="<%= BASE_URL %>static/favicon.png" />
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -6,7 +6,7 @@ import store from '../store';
import { models } from '../constants';
const http = axios.create({
baseURL: process.env.BASE_URL,
baseURL: process.env.VUE_APP_API_URL || '/',
withCredentials: false,
responseType: 'json',

View file

@ -1,5 +1,5 @@
module.exports = {
publicPath: '/',
publicPath: '/admin',
outputDir: 'dist',
// This is to make all static file requests generated by Vue to go to
@ -7,10 +7,10 @@ module.exports = {
// directory and moves all the static files in it. The physical directory
// and the URI for assets are tightly coupled. This is handled in the Go app
// by using stuffbin aliases.
assetsDir: 'frontend',
assetsDir: 'static',
// Move the index.html file from dist/index.html to dist/frontend/index.html
indexPath: './frontend/index.html',
// indexPath: './frontend/index.html',
productionSourceMap: false,
filenameHashing: true,
@ -26,7 +26,7 @@ module.exports = {
devServer: {
port: process.env.LISTMONK_FRONTEND_PORT || 8080,
proxy: {
'^/(api|webhooks|subscription|public)': {
'^/(api|webhooks|subscription|public)|$': {
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
}
}