Nice api landing & 404 pages
This commit is contained in:
+1
-1
@@ -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() {
|
||||
|
||||
+7
-52
@@ -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
|
||||
}
|
||||
|
||||
+60
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="/auth/assets/public/style.css?2022-02-21T16%3a52%3a49%2b01%3a00" rel="stylesheet">
|
||||
<title>Page not found | Corteza</title>
|
||||
<style>
|
||||
body {
|
||||
color: white;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
grid-template-rows: auto 150px;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
main {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
main h1 {
|
||||
font-size: 2500%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-bottom: -50px;
|
||||
text-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
main p {
|
||||
font-size: 200%;
|
||||
text-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body style="background: url(/assets/release-background.png) no-repeat top;background-size: cover;background-attachment: fixed;">
|
||||
<main>
|
||||
<div>
|
||||
<h1>404</h1>
|
||||
<p>
|
||||
It looks like the page you're looking for does not exist.
|
||||
</p>
|
||||
|
||||
<!-- links -->
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<footer class="p-5 text-white">
|
||||
Access source code on<a href="https://github.com/cortezaproject/" target="_blank" class="text-white ml-2">GitHub</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="/auth/assets/public/style.css?2022-02-21T16%3a52%3a49%2b01%3a00" rel="stylesheet">
|
||||
<title>Corteza API</title>
|
||||
<style>
|
||||
body {
|
||||
color: white;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
grid-template-rows: auto 150px;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
main {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
main h1 {
|
||||
font-size: 2500%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-bottom: -50px;
|
||||
text-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
main p {
|
||||
font-size: 200%;
|
||||
text-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body style="background: url(/assets/release-background.png) no-repeat top;background-size: cover;background-attachment: fixed;">
|
||||
<main>
|
||||
<div>
|
||||
<h1>HEY</h1>
|
||||
<p>
|
||||
How are you? Thank you for stopping by,
|
||||
<br />here is what you can do:
|
||||
</p>
|
||||
|
||||
<!-- links -->
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<footer class="p-5 text-white">
|
||||
Access source code on<a href="https://github.com/cortezaproject/" target="_blank" class="text-white ml-2">GitHub</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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 = `<a class="btn btn-light font-weight-bold text-dark m-2" href="%s">%s</a>`
|
||||
var (
|
||||
links = make([]string, 0)
|
||||
buf []byte
|
||||
|
||||
placeholder = []byte("<!-- links -->")
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-14
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user