mirror of
https://github.com/h44z/wg-portal.git
synced 2025-12-16 03:26:17 +00:00
allow setting a base-path for the web UI and API (#583)
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 709 B |
@@ -6,9 +6,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>WireGuard Portal API</title>
|
||||
<meta name="description" content="WireGuard Portal API">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/app/favicon.ico">
|
||||
<link rel="stylesheet" href="{{$.BasePath}}/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{$.BasePath}}/fonts/fontawesome-all.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{$.BasePath}}/app/favicon.ico">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="card-header">SPA Api</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This endpoint is used by the Single Page Application (the Frontend/UI).</p>
|
||||
<a href="/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="{{$.BasePath}}/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="card-header"><b>Version 1</b></div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This is the current main API endpoint.</p>
|
||||
<a href="/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="{{$.BasePath}}/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,17 +43,17 @@
|
||||
<div class="card-header">Version 2</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This will be a future API version, it is currently work in progress.</p>
|
||||
<a href="/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="{{$.BasePath}}/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.gohtml" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/jquery.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/jquery.easing.js"></script>
|
||||
<script src="{{$.BasePath}}/js/popper.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="/img/header-logo.png" alt="Prolicht"/></a>
|
||||
<a class="navbar-brand" href="/"><img src="{{$.BasePath}}/img/header-logo.png" alt="Prolicht"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||
</ul>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
allow-spec-file-load="false"
|
||||
allow-spec-file-download="true"
|
||||
>
|
||||
<img slot="logo" src="/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||
<img slot="logo" src="{{$.BasePath}}/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||
|
||||
<p slot="footer" style="margin:0; padding:16px 36px; background-color:#f76b39; color:#fff; text-align:center;" >
|
||||
Copyright © WireGuard Portal {{$.Year}}, version {{$.Version}}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
@@ -37,6 +39,7 @@ type ApiEndpointSetupFunc func() (ApiVersion, GroupSetupFn)
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
server *routegroup.Bundle
|
||||
root *routegroup.Bundle // root is the web-root (potentially with path prefix)
|
||||
tpl *respond.TemplateRenderer
|
||||
versions map[ApiVersion]*routegroup.Bundle
|
||||
}
|
||||
@@ -77,13 +80,42 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
||||
template.Must(template.New("").ParseFS(apiTemplates, "assets/tpl/*.gohtml")),
|
||||
)
|
||||
|
||||
// Serve static files
|
||||
// Mount base path if configured
|
||||
s.root = s.server
|
||||
if s.cfg.Web.BasePath != "" {
|
||||
s.root = s.server.Mount(s.cfg.Web.BasePath)
|
||||
}
|
||||
|
||||
// Serve static files (under base path if configured)
|
||||
imgFs := http.FS(fsMust(fs.Sub(apiStatics, "assets/img")))
|
||||
s.server.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||
s.server.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||
s.server.HandleFiles("/img", imgFs)
|
||||
s.server.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||
s.server.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
s.root.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||
s.root.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||
s.root.HandleFiles("/img", imgFs)
|
||||
s.root.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||
if cfg.Web.BasePath == "" {
|
||||
s.root.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
} else {
|
||||
customV0File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v0_swagger.yaml")
|
||||
customV1File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v1_swagger.yaml")
|
||||
customV0File = []byte(strings.Replace(string(customV0File),
|
||||
"basePath: /api/v0", "basePath: "+cfg.Web.BasePath+"/api/v0", 1))
|
||||
customV1File = []byte(strings.Replace(string(customV1File),
|
||||
"basePath: /api/v1", "basePath: "+cfg.Web.BasePath+"/api/v1", 1))
|
||||
|
||||
s.root.HandleFunc("GET /doc/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v0_swagger.yaml" {
|
||||
respond.Data(w, http.StatusOK, "application/yaml", customV0File)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v1_swagger.yaml" {
|
||||
respond.Data(w, http.StatusOK, "application/yaml", customV1File)
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
// Setup routes
|
||||
s.setupRoutes(endpoints...)
|
||||
@@ -128,14 +160,14 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
s.server.HandleFunc("GET /api", s.landingPage)
|
||||
s.root.HandleFunc("GET /api", s.landingPage)
|
||||
s.versions = make(map[ApiVersion]*routegroup.Bundle)
|
||||
|
||||
for _, setupFunc := range endpoints {
|
||||
version, groupSetupFn := setupFunc()
|
||||
|
||||
if _, ok := s.versions[version]; !ok {
|
||||
s.versions[version] = s.server.Mount(fmt.Sprintf("/api/%s", version))
|
||||
s.versions[version] = s.root.Mount(fmt.Sprintf("/api/%s", version))
|
||||
|
||||
// OpenAPI documentation (via RapiDoc)
|
||||
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
||||
@@ -149,16 +181,17 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
|
||||
func (s *Server) setupFrontendRoutes() {
|
||||
// Serve static files
|
||||
s.server.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app")
|
||||
s.root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app")
|
||||
})
|
||||
|
||||
s.server.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app/favicon.ico")
|
||||
s.root.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app/favicon.ico")
|
||||
})
|
||||
|
||||
// If a custom frontend path is configured, serve files from there when it contains content.
|
||||
// If the directory is empty or missing, populate it with the embedded frontend-dist content first.
|
||||
useEmbeddedFrontend := true
|
||||
if s.cfg.Web.FrontendFilePath != "" {
|
||||
if err := os.MkdirAll(s.cfg.Web.FrontendFilePath, 0755); err != nil {
|
||||
slog.Error("failed to create frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||
@@ -181,34 +214,93 @@ func (s *Server) setupFrontendRoutes() {
|
||||
if ok {
|
||||
// serve files from FS
|
||||
slog.Debug("serving frontend files from custom path", "path", s.cfg.Web.FrontendFilePath)
|
||||
s.server.HandleFiles("/app", http.Dir(s.cfg.Web.FrontendFilePath))
|
||||
return
|
||||
useEmbeddedFrontend = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: serve embedded frontend files
|
||||
s.server.HandleFiles("/app", http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||
var fileServer http.Handler
|
||||
if useEmbeddedFrontend {
|
||||
fileServer = http.FileServer(http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||
} else {
|
||||
fileServer = http.FileServer(http.Dir(s.cfg.Web.FrontendFilePath))
|
||||
}
|
||||
fileServer = http.StripPrefix(s.cfg.Web.BasePath+"/app", fileServer)
|
||||
|
||||
// Modify index.html and CSS to include the correct base path.
|
||||
var customIndexFile, customCssFile []byte
|
||||
var customCssFileName string
|
||||
if s.cfg.Web.BasePath != "" {
|
||||
customIndexFile, customCssFile, customCssFileName = s.updateBasePathInFrontend(useEmbeddedFrontend)
|
||||
}
|
||||
|
||||
s.root.HandleFunc("GET /app/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// serve a custom index.html file with the correct base path applied
|
||||
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/" {
|
||||
respond.Data(w, http.StatusOK, "text/html", customIndexFile)
|
||||
return
|
||||
}
|
||||
|
||||
// serve a custom CSS file with the correct base path applied
|
||||
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/assets/"+customCssFileName {
|
||||
respond.Data(w, http.StatusOK, "text/css", customCssFile)
|
||||
return
|
||||
}
|
||||
|
||||
// pass all other requests to the file server
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) landingPage(w http.ResponseWriter, _ *http.Request) {
|
||||
s.tpl.HTML(w, http.StatusOK, "index.gohtml", respond.TplData{
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
"BasePath": s.cfg.Web.BasePath,
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rapiDocHandler(version ApiVersion) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.tpl.HTML(w, http.StatusOK, "rapidoc.gohtml", respond.TplData{
|
||||
"RapiDocSource": "/js/rapidoc-min.js",
|
||||
"ApiSpecUrl": fmt.Sprintf("/doc/%s_swagger.yaml", version),
|
||||
"RapiDocSource": s.cfg.Web.BasePath + "/js/rapidoc-min.js",
|
||||
"BasePath": s.cfg.Web.BasePath,
|
||||
"ApiSpecUrl": fmt.Sprintf("%s/doc/%s_swagger.yaml", s.cfg.Web.BasePath, version),
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []byte, string) {
|
||||
if s.cfg.Web.BasePath == "" {
|
||||
return nil, nil, "" // nothing to do
|
||||
}
|
||||
|
||||
var customIndexFile []byte
|
||||
if useEmbeddedFrontend {
|
||||
customIndexFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "index.html")
|
||||
} else {
|
||||
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
|
||||
}
|
||||
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+s.cfg.Web.BasePath+"/")
|
||||
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"/", "href=\""+s.cfg.Web.BasePath+"/")
|
||||
|
||||
re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
|
||||
match := re.FindStringSubmatch(newIndexStr)
|
||||
cssFileName := match[1]
|
||||
|
||||
var customCssFile []byte
|
||||
if useEmbeddedFrontend {
|
||||
customCssFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "assets/"+cssFileName)
|
||||
} else {
|
||||
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
|
||||
}
|
||||
newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
|
||||
|
||||
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
||||
}
|
||||
|
||||
func fsMust(f fs.FS, err error) fs.FS {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -67,7 +67,8 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
// @Router /config/frontend.js [get]
|
||||
func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
backendUrl := fmt.Sprintf("%s/api/v0", e.cfg.Web.ExternalUrl)
|
||||
basePath := e.cfg.Web.BasePath
|
||||
backendUrl := fmt.Sprintf("%s%s/api/v0", e.cfg.Web.ExternalUrl, basePath)
|
||||
if request.Header(r, "x-wg-dev") != "" {
|
||||
referer := request.Header(r, "Referer")
|
||||
host := "localhost"
|
||||
@@ -76,12 +77,13 @@ func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
if err == nil {
|
||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||
}
|
||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||
port) // override if request comes from frontend started with npm run dev
|
||||
backendUrl = fmt.Sprintf("http://%s:%s%s/api/v0", host,
|
||||
port, basePath) // override if request comes from frontend started with npm run dev
|
||||
}
|
||||
|
||||
e.tpl.Render(w, http.StatusOK, "frontend_config.js.gotpl", "text/javascript", map[string]any{
|
||||
"BackendUrl": backendUrl,
|
||||
"BasePath": basePath,
|
||||
"Version": internal.Version,
|
||||
"SiteTitle": e.cfg.Web.SiteTitle,
|
||||
"SiteCompanyName": e.cfg.Web.SiteCompanyName,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
WGPORTAL_BACKEND_BASE_URL="{{ $.BackendUrl }}";
|
||||
WGPORTAL_BASE_PATH="{{ $.BasePath }}";
|
||||
WGPORTAL_VERSION="{{ $.Version }}";
|
||||
WGPORTAL_SITE_TITLE="{{ $.SiteTitle }}";
|
||||
WGPORTAL_SITE_COMPANY_NAME="{{ $.SiteCompanyName }}";
|
||||
|
||||
@@ -49,7 +49,11 @@ func NewSessionWrapper(cfg *config.Config) *SessionWrapper {
|
||||
sessionManager.Cookie.Secure = strings.HasPrefix(cfg.Web.ExternalUrl, "https")
|
||||
sessionManager.Cookie.HttpOnly = true
|
||||
sessionManager.Cookie.SameSite = http.SameSiteLaxMode
|
||||
sessionManager.Cookie.Path = "/"
|
||||
if cfg.Web.BasePath != "" {
|
||||
sessionManager.Cookie.Path = cfg.Web.BasePath
|
||||
} else {
|
||||
sessionManager.Cookie.Path = "/"
|
||||
}
|
||||
sessionManager.Cookie.Persist = false
|
||||
|
||||
wrappedSessionManager := &SessionWrapper{sessionManager}
|
||||
|
||||
@@ -99,7 +99,7 @@ type Authenticator struct {
|
||||
}
|
||||
|
||||
// NewAuthenticator creates a new Authenticator instance.
|
||||
func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserManager) (
|
||||
func NewAuthenticator(cfg *config.Auth, extUrl, basePath string, bus EventBus, users UserManager) (
|
||||
*Authenticator,
|
||||
error,
|
||||
) {
|
||||
@@ -107,7 +107,7 @@ func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserM
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
users: users,
|
||||
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
||||
callbackUrlPrefix: fmt.Sprintf("%s%s/api/v0", extUrl, basePath),
|
||||
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
|
||||
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user