diff --git a/app/cli.go b/app/cli.go
index 5d9b0e770..b88946e38 100644
--- a/app/cli.go
+++ b/app/cli.go
@@ -74,7 +74,7 @@ func (app *CortezaApp) InitCLI() {
wg := &sync.WaitGroup{}
{ // @todo refactor wait-for out of HTTP API server.
- app.HttpServer = server.New(app.Log, app.Opt.Environment, app.Opt.HTTPServer, app.Opt.WaitFor)
+ app.HttpServer = server.New(app.Log, app.Opt)
wg.Add(1)
go func() {
diff --git a/app/servers.go b/app/servers.go
index 12cd2808f..2d3a83c37 100644
--- a/app/servers.go
+++ b/app/servers.go
@@ -1,10 +1,7 @@
package app
import (
- "fmt"
- "io/fs"
"net/http"
- "os"
"path"
"regexp"
"strings"
@@ -35,31 +32,16 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
var (
url = options.CleanBase(ho.BaseUrl, "assets")
aPath = ho.AssetsPath
- files fs.FS
- err error
+ files = assets.Files(app.Log, aPath)
)
- if len(aPath) > 0 {
- if files, err = loadAssetsFromPath(aPath); err != nil {
- // log warning but fallback to embedded assets
- app.Log.Warn(
- fmt.Sprintf("failed to use custom assets path (HTTP_SERVER_ASSETS_PATH=%s)", aPath),
- zap.Error(err),
- )
- }
- }
-
- if files == nil {
- aPath = "embedded"
- files, err = fs.Sub(assets.Embedded, "src")
- if err != nil {
- // if this is off, we might as well panic
- panic(err)
- }
- }
-
r.Handle(url+"/*", http.StripPrefix(url+"/", http.FileServer(http.FS(files))))
- app.Log.Info("web assets mounted", zap.String("url", url), zap.String("path", aPath))
+
+ if aPath != "" {
+ app.Log.Info("custom web assets mounted", zap.String("url", url), zap.String("path", aPath))
+ } else {
+ app.Log.Info("embedded web assets mounted", zap.String("url", url))
+ }
}()
func() {
@@ -171,30 +153,3 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
r.Handle("/.well-known/openid-configuration", app.AuthService.WellKnownOpenIDConfiguration())
}()
}
-
-func loadAssetsFromPath(path string) (assets fs.FS, err error) {
- // at least favicon file should exist in the custom asset path
- // otherwise we default to embedded files
- const check = "favicon32x32.png"
-
- var (
- fi os.FileInfo
- )
-
- if fi, err = os.Stat(path); err != nil {
- return
-
- }
-
- if !fi.IsDir() {
- return nil, fmt.Errorf("expecting directory")
-
- }
-
- assets = os.DirFS(path)
- if _, err = assets.Open(check); err != nil {
- return nil, err
- }
-
- return
-}
diff --git a/assets/embed.go b/assets/embed.go
index 49a2a281e..646db83fc 100644
--- a/assets/embed.go
+++ b/assets/embed.go
@@ -2,9 +2,68 @@ package assets
import (
"embed"
+ "fmt"
+ "io/fs"
+ "os"
+
+ "go.uber.org/zap"
)
var (
//go:embed src/*
- Embedded embed.FS
+ ff embed.FS
)
+
+// Files return files from assets path or falls back to embedded files
+//
+// @todo try to find a way to merge this with auth/assets
+func Files(log *zap.Logger, aPath string) (files fs.FS) {
+ var err error
+ if len(aPath) > 0 {
+ if files, err = fromPath(aPath); err != nil {
+ // log warning but fallback to embedded assets
+ log.Warn(
+ fmt.Sprintf("failed to use custom assets path (HTTP_SERVER_ASSETS_PATH=%s)", aPath),
+ zap.Error(err),
+ )
+ }
+ }
+
+ if files == nil {
+ aPath = "embedded"
+ files, err = fs.Sub(ff, "src")
+ if err != nil {
+ // something is seriously wrong, we might as well panic
+ panic(err)
+ }
+ }
+
+ return
+}
+
+func fromPath(path string) (assets fs.FS, err error) {
+ // at least favicon file should exist in the custom asset path
+ // otherwise we default to embedded files
+ const check = "favicon32x32.png"
+
+ var (
+ fi os.FileInfo
+ )
+
+ if fi, err = os.Stat(path); err != nil {
+ return
+
+ }
+
+ if !fi.IsDir() {
+ return nil, fmt.Errorf("expecting directory")
+
+ }
+
+ assets = os.DirFS(path)
+ if _, err = assets.Open(check); err != nil {
+ return nil, err
+ }
+
+ return
+}
diff --git a/assets/src/api-404.html b/assets/src/api-404.html
new file mode 100644
index 000000000..24404785e
--- /dev/null
+++ b/assets/src/api-404.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Page not found | Corteza
+
+
+
+
+
+
404
+
+ It looks like the page you're looking for does not exist.
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/src/api-landing.html b/assets/src/api-landing.html
new file mode 100644
index 000000000..24134a75b
--- /dev/null
+++ b/assets/src/api-landing.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Corteza API
+
+
+
+
+
+
HEY
+
+ How are you? Thank you for stopping by,
+
here is what you can do:
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/api/server/handlers.go b/pkg/api/server/handlers.go
index 3364d982c..6cf82f359 100644
--- a/pkg/api/server/handlers.go
+++ b/pkg/api/server/handlers.go
@@ -1,12 +1,15 @@
package server
import (
+ "bytes"
"fmt"
+ "io"
"net/http"
"path"
"strconv"
"strings"
+ "github.com/cortezaproject/corteza-server/assets"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
@@ -71,10 +74,14 @@ func shutdownRoutes() (r chi.Router) {
}
// routes used when in active mode
-func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt) (r chi.Router) {
+func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), opts *options.Options) (r chi.Router) {
r = chi.NewRouter()
r.Use(handleCORS)
+ httpOpt := opts.HTTPServer
+ authOpt := opts.Auth
+ envOpt := opts.Environment
+
r.Route("/"+strings.TrimPrefix(httpOpt.BaseUrl, "/"), func(r chi.Router) {
// Reports error to Sentry if enabled
if httpOpt.EnablePanicReporting {
@@ -120,6 +127,11 @@ func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), envOpt option
}
mountServiceHandlers(r, log, httpOpt, active)
+
+ r.HandleFunc(httpOpt.ApiBaseUrl, handleStaticPages(log, httpOpt, authOpt, "api-landing.html"))
+ r.HandleFunc(httpOpt.ApiBaseUrl+"/", handleStaticPages(log, httpOpt, authOpt, "api-landing.html"))
+ r.NotFound(handleStaticPages(log, httpOpt, authOpt, "api-404.html"))
+
return
}
@@ -252,3 +264,48 @@ func mountDebugLogViewer(r chi.Router, log *zap.Logger) {
_, _ = logger.WriteLogBuffer(w, after, limit)
})
}
+
+func handleStaticPages(log *zap.Logger, hOpt options.HttpServerOpt, aOpt options.AuthOpt, file string) http.HandlerFunc {
+ // "good-enough" for now, plan to move to templates when
+ // merging with auth
+ const linkTpl = `%s`
+ var (
+ links = make([]string, 0)
+ buf []byte
+
+ placeholder = []byte("")
+ )
+
+ links = append(links, fmt.Sprintf(linkTpl, aOpt.BaseURL, "Login"))
+
+ if hOpt.ApiEnabled {
+ links = append(links, fmt.Sprintf(linkTpl, "https://docs.cortezaproject.org/", "Documentation"))
+ }
+
+ if hOpt.WebConsoleEnabled {
+ links = append(links, fmt.Sprintf(linkTpl, "/console", "Console"))
+ }
+
+ page, err := assets.Files(log, hOpt.AssetsPath).Open(file)
+ if err != nil {
+ log.Warn("could not open static page", zap.String("file", file), zap.Error(err))
+ }
+
+ buf, err = io.ReadAll(page)
+ if err != nil {
+ log.Warn("could not prepare static page", zap.String("file", file), zap.Error(err))
+ }
+
+ buf = bytes.ReplaceAll(buf, placeholder, []byte(strings.Join(links, "")))
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ if page == nil {
+ // fallback to default 404 handler
+ http.NotFound(w, r)
+ return
+ }
+
+ w.WriteHeader(http.StatusNotFound)
+ w.Write(buf)
+ }
+}
diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go
index 0dd2f832c..89fb22c43 100644
--- a/pkg/api/server/server.go
+++ b/pkg/api/server/server.go
@@ -12,11 +12,9 @@ import (
type (
server struct {
- log *zap.Logger
- httpOpt options.HttpServerOpt
- waitForOpt options.WaitForOpt
- environmentOpt options.EnvironmentOpt
- endpoints []func(r chi.Router)
+ log *zap.Logger
+ opts *options.Options
+ endpoints []func(r chi.Router)
demux *demux
}
@@ -38,17 +36,15 @@ const (
// - /healthcheck
// - /healthcheck
-func New(log *zap.Logger, envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt, waitForOpt options.WaitForOpt) *server {
+func New(log *zap.Logger, opts *options.Options) *server {
s := &server{
endpoints: make([]func(r chi.Router), 0),
log: log.Named("http"),
- environmentOpt: envOpt,
- httpOpt: httpOpt,
- waitForOpt: waitForOpt,
+ opts: opts,
}
- s.demux = Demux(waiting, waitingRoutes(s.log.Named("waiting"), s.httpOpt))
+ s.demux = Demux(waiting, waitingRoutes(s.log.Named("waiting"), s.opts.HTTPServer))
s.demux.Router(shutdown, shutdownRoutes())
return s
@@ -56,7 +52,7 @@ func New(log *zap.Logger, envOpt options.EnvironmentOpt, httpOpt options.HttpSer
// Activate reconfigures server to use active routes
func (s *server) Activate(mm ...func(chi.Router)) {
- s.demux.Router(active, activeRoutes(s.log, mm, s.environmentOpt, s.httpOpt))
+ s.demux.Router(active, activeRoutes(s.log, mm, s.opts))
s.log.Debug("entering active state")
s.demux.State(active)
@@ -72,11 +68,11 @@ func (s server) Serve(ctx context.Context) {
s.log.Info(
"starting HTTP server",
- zap.String("path-prefix", s.httpOpt.BaseUrl),
- zap.String("address", s.httpOpt.Addr),
+ zap.String("path-prefix", s.opts.HTTPServer.BaseUrl),
+ zap.String("address", s.opts.HTTPServer.Addr),
)
- listener, err := net.Listen("tcp", s.httpOpt.Addr)
+ listener, err := net.Listen("tcp", s.opts.HTTPServer.Addr)
if err != nil {
s.log.Error("cannot start server", zap.Error(err))
return
diff --git a/pkg/api/server/waitfor.go b/pkg/api/server/waitfor.go
index 025ef7ae4..df24de0e2 100644
--- a/pkg/api/server/waitfor.go
+++ b/pkg/api/server/waitfor.go
@@ -18,7 +18,7 @@ import (
// WaitFor sets up a simple status page, delays execution and probes services
func (s server) WaitFor(ctx context.Context) {
var (
- opt = s.waitForOpt
+ opt = s.opts.WaitFor
services = opt.GetServices()
)
@@ -37,7 +37,7 @@ func (s server) WaitFor(ctx context.Context) {
)
// Setup a simple HTTP server that will inform the impatient users
- listener, err := net.Listen("tcp", s.httpOpt.Addr)
+ listener, err := net.Listen("tcp", s.opts.HTTPServer.Addr)
if err != nil {
s.log.Error("cannot start server", zap.Error(err))
os.Exit(1)