Allow custom URL prefix for all corteza endpoints
Add new env. variables and options - HTTP_BASE_URL to control URL prefix, defaults to / - HTTP_SSL_TERMINATED to explicitly set if Corteza is running behind HTTPS Refresh and document webapp/Makefile with more dev tasks Fix all absolute URLs in applications, logos, icons Improve logic behind integrated webapp serving, inject/replace <base href> tag according to URL prefix Prevent mounting api & webapps to the same base
This commit is contained in:
@@ -143,7 +143,7 @@ provision:
|
||||
$(MAKE) --directory=provision clean all
|
||||
|
||||
webapp:
|
||||
$(MAKE) --directory=webapp install
|
||||
@ $(MAKE) --directory=webapp
|
||||
|
||||
|
||||
#######################################################################################################################
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ type (
|
||||
}
|
||||
|
||||
authServicer interface {
|
||||
MountHttpRoutes(chi.Router)
|
||||
MountHttpRoutes(string, chi.Router)
|
||||
UpdateSettings(*settings.Settings)
|
||||
Watch(ctx context.Context)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
authHandlers "github.com/cortezaproject/corteza-server/auth/handlers"
|
||||
"strings"
|
||||
|
||||
authService "github.com/cortezaproject/corteza-server/auth"
|
||||
@@ -87,6 +88,9 @@ func (app *CortezaApp) Setup() (err error) {
|
||||
// that might occur inside auth, mail setup...
|
||||
defer sentry.Recover()
|
||||
|
||||
// set base path for links&routes in auth server
|
||||
authHandlers.BasePath = app.Opt.HTTPServer.BaseUrl
|
||||
|
||||
auth.SetupDefault(app.Opt.Auth.Secret, app.Opt.Auth.Expiry)
|
||||
|
||||
mail.SetupDialer(
|
||||
|
||||
+51
-35
@@ -9,12 +9,14 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api/server"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
"github.com/cortezaproject/corteza-server/pkg/webapp"
|
||||
systemRest "github.com/cortezaproject/corteza-server/system/rest"
|
||||
"github.com/cortezaproject/corteza-server/system/scim"
|
||||
"github.com/go-chi/chi"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -50,15 +52,48 @@ func (app *CortezaApp) Serve(ctx context.Context) (err error) {
|
||||
|
||||
func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
var (
|
||||
apiBaseUrl = strings.Trim(app.Opt.HTTPServer.ApiBaseUrl, "/")
|
||||
webappBaseUrl = strings.Trim(app.Opt.HTTPServer.WebappBaseUrl, "/")
|
||||
ho = app.Opt.HTTPServer
|
||||
)
|
||||
|
||||
app.AuthService.MountHttpRoutes(r)
|
||||
func() {
|
||||
if ho.ApiEnabled && ho.ApiBaseUrl == ho.WebappBaseUrl {
|
||||
app.Log.
|
||||
WithOptions(zap.AddStacktrace(zap.PanicLevel)).
|
||||
Warn("client web applications and api can not use the same base URL: " + ho.WebappBaseUrl)
|
||||
ho.WebappEnabled = false
|
||||
}
|
||||
|
||||
if app.Opt.HTTPServer.ApiEnabled {
|
||||
if !ho.WebappEnabled {
|
||||
app.Log.Info("client web applications disabled")
|
||||
return
|
||||
}
|
||||
|
||||
r.Route("/"+ho.WebappBaseUrl, webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth))
|
||||
|
||||
app.Log.Info(
|
||||
"client web applications enabled",
|
||||
zap.String("baseUrl", options.CleanBase(ho.BaseUrl, ho.WebappBaseUrl)),
|
||||
zap.String("baseDir", ho.WebappBaseDir),
|
||||
zap.Strings("apps", strings.Split(ho.WebappList, ",")),
|
||||
)
|
||||
}()
|
||||
|
||||
// Auth server
|
||||
app.AuthService.MountHttpRoutes(ho.BaseUrl, r)
|
||||
|
||||
func() {
|
||||
if !ho.ApiEnabled {
|
||||
app.Log.Info("JSON REST API disabled")
|
||||
}
|
||||
|
||||
r.Route(ho.ApiBaseUrl, func(r chi.Router) {
|
||||
var fullpathAPI = options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl)
|
||||
|
||||
app.Log.Info(
|
||||
"JSON REST API enabled",
|
||||
zap.String("baseUrl", fullpathAPI),
|
||||
)
|
||||
|
||||
r.Route("/"+apiBaseUrl, func(r chi.Router) {
|
||||
r.Route("/system", systemRest.MountRoutes)
|
||||
r.Route("/automation", automationRest.MountRoutes)
|
||||
r.Route("/compose", composeRest.MountRoutes)
|
||||
@@ -67,22 +102,16 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
r.Route("/federation", federationRest.MountRoutes)
|
||||
}
|
||||
|
||||
r.Handle("/docs", http.RedirectHandler("/"+apiBaseUrl+"/docs/", http.StatusPermanentRedirect))
|
||||
r.Handle("/docs*", http.StripPrefix("/"+apiBaseUrl+"/docs", http.FileServer(docs.GetFS())))
|
||||
var fullpathDocs = options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl, "docs")
|
||||
app.Log.Info(
|
||||
"API docs enabled",
|
||||
zap.String("baseUrl", fullpathDocs),
|
||||
)
|
||||
|
||||
r.Handle("/docs", http.RedirectHandler(fullpathDocs+"/", http.StatusPermanentRedirect))
|
||||
r.Handle("/docs*", http.StripPrefix(fullpathDocs, http.FileServer(docs.GetFS())))
|
||||
})
|
||||
|
||||
app.Log.Info(
|
||||
"JSON REST API enabled",
|
||||
zap.String("baseUrl", app.Opt.HTTPServer.ApiBaseUrl),
|
||||
)
|
||||
|
||||
app.Log.Info(
|
||||
"API docs enabled",
|
||||
zap.String("baseUrl", app.Opt.HTTPServer.ApiBaseUrl+"/docs"),
|
||||
)
|
||||
} else {
|
||||
app.Log.Info("JSON REST API disabled")
|
||||
}
|
||||
}()
|
||||
|
||||
func() {
|
||||
if !app.Opt.SCIM.Enabled {
|
||||
@@ -96,7 +125,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
}
|
||||
|
||||
var (
|
||||
baseUrl = "/" + strings.Trim(app.Opt.SCIM.BaseURL, "/")
|
||||
baseUrl = app.Opt.SCIM.BaseURL
|
||||
extIdValidation *regexp.Regexp
|
||||
err error
|
||||
)
|
||||
@@ -112,7 +141,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
|
||||
app.Log.Debug(
|
||||
"SCIM enabled",
|
||||
zap.String("baseUrl", baseUrl),
|
||||
zap.String("baseUrl", path.Join(app.Opt.HTTPServer.BaseUrl, baseUrl)),
|
||||
logger.Mask("secret", app.Opt.SCIM.Secret),
|
||||
)
|
||||
|
||||
@@ -127,17 +156,4 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
})
|
||||
})
|
||||
}()
|
||||
|
||||
if app.Opt.HTTPServer.WebappEnabled {
|
||||
r.Route("/"+webappBaseUrl, webapp.MakeWebappServer(app.Opt.HTTPServer, app.Opt.Auth, app.Opt.Federation))
|
||||
|
||||
app.Log.Info(
|
||||
"client web applications enabled",
|
||||
zap.String("baseUrl", app.Opt.HTTPServer.WebappBaseUrl),
|
||||
zap.String("baseDir", app.Opt.HTTPServer.WebappBaseDir),
|
||||
zap.Strings("apps", strings.Split(app.Opt.HTTPServer.WebappList, ",")),
|
||||
)
|
||||
} else {
|
||||
app.Log.Info("client web applications disabled")
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -218,6 +218,12 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO
|
||||
|
||||
external.Init(log, sesManager.Store())
|
||||
|
||||
svc.log.Info(
|
||||
"auth server ready",
|
||||
zap.String("AUTH_BASE_URL", svc.opt.BaseURL),
|
||||
zap.String("AUTH_EXTERNAL_REDIRECT_URL", svc.opt.ExternalRedirectURL),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +306,8 @@ func (svc service) gcOAuth2Tokens(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) MountHttpRoutes(r chi.Router) {
|
||||
func (svc service) MountHttpRoutes(basePath string, r chi.Router) {
|
||||
basePath = strings.TrimRight(basePath, "/")
|
||||
svc.handlers.MountHttpRoutes(r)
|
||||
|
||||
const uriRoot = "/auth/assets/public"
|
||||
@@ -314,13 +321,13 @@ func (svc service) MountHttpRoutes(r chi.Router) {
|
||||
zap.String("AUTH_ASSETS_PATH", svc.opt.AssetsPath),
|
||||
)
|
||||
} else {
|
||||
r.Handle(uriRoot+"/*", http.StripPrefix(uriRoot, http.FileServer(http.Dir(root))))
|
||||
r.Handle(uriRoot+"/*", http.StripPrefix(basePath+uriRoot, http.FileServer(http.Dir(root))))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to embedded assets
|
||||
r.Handle(uriRoot+"/*", http.StripPrefix("/auth/", http.FileServer(http.FS(PublicAssets))))
|
||||
r.Handle(uriRoot+"/*", http.StripPrefix(basePath+"/auth/", http.FileServer(http.FS(PublicAssets))))
|
||||
}
|
||||
|
||||
// checks if directory exists & is readable
|
||||
|
||||
+40
-24
@@ -1,5 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import "strings"
|
||||
|
||||
type (
|
||||
Links struct {
|
||||
Profile,
|
||||
@@ -36,35 +38,49 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var BasePath string = "/"
|
||||
|
||||
func GetLinks() Links {
|
||||
var b = BasePath + "/"
|
||||
|
||||
return Links{
|
||||
Profile: "/auth",
|
||||
Signup: "/auth/signup",
|
||||
ConfirmEmail: "/auth/confirm-email",
|
||||
PendingEmailConfirmation: "/auth/pending-email-confirmation",
|
||||
Login: "/auth/login",
|
||||
Security: "/auth/security",
|
||||
ChangePassword: "/auth/change-password",
|
||||
RequestPasswordReset: "/auth/request-password-reset",
|
||||
PasswordResetRequested: "/auth/password-reset-requested",
|
||||
ResetPassword: "/auth/reset-password",
|
||||
Sessions: "/auth/sessions",
|
||||
AuthorizedClients: "/auth/authorized-clients",
|
||||
Logout: "/auth/logout",
|
||||
Profile: b + "auth",
|
||||
Signup: b + "auth/signup",
|
||||
ConfirmEmail: b + "auth/confirm-email",
|
||||
PendingEmailConfirmation: b + "auth/pending-email-confirmation",
|
||||
Login: b + "auth/login",
|
||||
Security: b + "auth/security",
|
||||
ChangePassword: b + "auth/change-password",
|
||||
RequestPasswordReset: b + "auth/request-password-reset",
|
||||
PasswordResetRequested: b + "auth/password-reset-requested",
|
||||
ResetPassword: b + "auth/reset-password",
|
||||
Sessions: b + "auth/sessions",
|
||||
AuthorizedClients: b + "auth/authorized-clients",
|
||||
Logout: b + "auth/logout",
|
||||
|
||||
OAuth2Authorize: "/auth/oauth2/authorize",
|
||||
OAuth2AuthorizeClient: "/auth/oauth2/authorize-client",
|
||||
OAuth2Token: "/auth/oauth2/token",
|
||||
OAuth2Info: "/auth/oauth2/info",
|
||||
OAuth2DefaultClient: "/auth/oauth2/default-client",
|
||||
OAuth2Authorize: b + "auth/oauth2/authorize",
|
||||
OAuth2AuthorizeClient: b + "auth/oauth2/authorize-client",
|
||||
OAuth2Token: b + "auth/oauth2/token",
|
||||
OAuth2Info: b + "auth/oauth2/info",
|
||||
OAuth2DefaultClient: b + "auth/oauth2/default-client",
|
||||
|
||||
Mfa: "/auth/mfa",
|
||||
MfaTotpNewSecret: "/auth/mfa/totp/setup",
|
||||
MfaTotpQRImage: "/auth/mfa/totp/qr.png",
|
||||
MfaTotpDisable: "/auth/mfa/totp/disable",
|
||||
Mfa: b + "auth/mfa",
|
||||
MfaTotpNewSecret: b + "auth/mfa/totp/setup",
|
||||
MfaTotpQRImage: b + "auth/mfa/totp/qr.png",
|
||||
MfaTotpDisable: b + "auth/mfa/totp/disable",
|
||||
|
||||
External: "/auth/external",
|
||||
External: b + "auth/external",
|
||||
|
||||
Assets: "/auth/assets/public",
|
||||
Assets: b + "auth/assets/public",
|
||||
}
|
||||
}
|
||||
|
||||
// trim base path
|
||||
func tbp(s string) string {
|
||||
s = strings.TrimPrefix(s, BasePath)
|
||||
if !strings.HasPrefix(s, "/") {
|
||||
s = "/" + s
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
+35
-35
@@ -44,57 +44,57 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
|
||||
csrf.CookieName(h.Opt.CsrfCookieName),
|
||||
))
|
||||
|
||||
r.Get(l.Profile, h.handle(authOnly(h.profileForm)))
|
||||
r.Post(l.Profile, h.handle(authOnly(h.profileProc)))
|
||||
r.Get(tbp(l.Profile), h.handle(authOnly(h.profileForm)))
|
||||
r.Post(tbp(l.Profile), h.handle(authOnly(h.profileProc)))
|
||||
|
||||
r.HandleFunc(l.Logout, h.handle(h.logoutProc))
|
||||
r.HandleFunc(tbp(l.Logout), h.handle(h.logoutProc))
|
||||
|
||||
r.Get(l.Sessions, h.handle(authOnly(h.sessionsView)))
|
||||
r.Post(l.Sessions, h.handle(authOnly(h.sessionsProc)))
|
||||
r.Get(tbp(l.Sessions), h.handle(authOnly(h.sessionsView)))
|
||||
r.Post(tbp(l.Sessions), h.handle(authOnly(h.sessionsProc)))
|
||||
|
||||
r.Get(l.AuthorizedClients, h.handle(authOnly(h.clientsView)))
|
||||
r.Post(l.AuthorizedClients, h.handle(authOnly(h.clientsProc)))
|
||||
r.Get(tbp(l.AuthorizedClients), h.handle(authOnly(h.clientsView)))
|
||||
r.Post(tbp(l.AuthorizedClients), h.handle(authOnly(h.clientsProc)))
|
||||
|
||||
r.Get(l.Signup, h.handle(h.onlyIfSignupEnabled(anonyOnly(h.signupForm))))
|
||||
r.Post(l.Signup, h.handle(h.onlyIfSignupEnabled(anonyOnly(h.signupProc))))
|
||||
r.Get(l.PendingEmailConfirmation, h.handle(h.pendingEmailConfirmation))
|
||||
r.Get(l.ConfirmEmail, h.handle(h.confirmEmail))
|
||||
r.Get(tbp(l.Signup), h.handle(h.onlyIfSignupEnabled(anonyOnly(h.signupForm))))
|
||||
r.Post(tbp(l.Signup), h.handle(h.onlyIfSignupEnabled(anonyOnly(h.signupProc))))
|
||||
r.Get(tbp(l.PendingEmailConfirmation), h.handle(h.pendingEmailConfirmation))
|
||||
r.Get(tbp(l.ConfirmEmail), h.handle(h.confirmEmail))
|
||||
|
||||
r.Get(l.Login, h.handle(anonyOnly(h.loginForm)))
|
||||
r.Post(l.Login, h.handle(h.onlyIfLocalEnabled(anonyOnly(h.loginProc))))
|
||||
r.Get(tbp(l.Login), h.handle(anonyOnly(h.loginForm)))
|
||||
r.Post(tbp(l.Login), h.handle(h.onlyIfLocalEnabled(anonyOnly(h.loginProc))))
|
||||
|
||||
r.Get(l.Mfa, h.handle(h.mfaForm))
|
||||
r.Post(l.Mfa, h.handle(h.mfaProc))
|
||||
r.Get(tbp(l.Mfa), h.handle(h.mfaForm))
|
||||
r.Post(tbp(l.Mfa), h.handle(h.mfaProc))
|
||||
|
||||
r.Get(l.RequestPasswordReset, h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.requestPasswordResetForm))))
|
||||
r.Post(l.RequestPasswordReset, h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.requestPasswordResetProc))))
|
||||
r.Get(l.PasswordResetRequested, h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.passwordResetRequested))))
|
||||
r.Get(l.ResetPassword, h.handle(h.onlyIfPasswordResetEnabled(h.resetPasswordForm)))
|
||||
r.Post(l.ResetPassword, h.handle(h.onlyIfPasswordResetEnabled(authOnly(h.resetPasswordProc))))
|
||||
r.Get(tbp(l.RequestPasswordReset), h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.requestPasswordResetForm))))
|
||||
r.Post(tbp(l.RequestPasswordReset), h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.requestPasswordResetProc))))
|
||||
r.Get(tbp(l.PasswordResetRequested), h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.passwordResetRequested))))
|
||||
r.Get(tbp(l.ResetPassword), h.handle(h.onlyIfPasswordResetEnabled(h.resetPasswordForm)))
|
||||
r.Post(tbp(l.ResetPassword), h.handle(h.onlyIfPasswordResetEnabled(authOnly(h.resetPasswordProc))))
|
||||
|
||||
r.Get(l.Security, h.handle(authOnly(h.securityForm)))
|
||||
r.Post(l.Security, h.handle(authOnly(h.securityProc)))
|
||||
r.Get(l.ChangePassword, h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordForm))))
|
||||
r.Post(l.ChangePassword, h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordProc))))
|
||||
r.Get(tbp(l.Security), h.handle(authOnly(h.securityForm)))
|
||||
r.Post(tbp(l.Security), h.handle(authOnly(h.securityProc)))
|
||||
r.Get(tbp(l.ChangePassword), h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordForm))))
|
||||
r.Post(tbp(l.ChangePassword), h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordProc))))
|
||||
|
||||
r.Get(l.MfaTotpNewSecret, h.handle(partAuthOnly(h.mfaTotpConfigForm)))
|
||||
r.Post(l.MfaTotpNewSecret, h.handle(partAuthOnly(h.mfaTotpConfigProc)))
|
||||
r.Get(l.MfaTotpQRImage, h.handle(partAuthOnly(h.mfaTotpConfigQR)))
|
||||
r.Get(l.MfaTotpDisable, h.handle(authOnly(h.mfaTotpDisableForm)))
|
||||
r.Post(l.MfaTotpDisable, h.handle(authOnly(h.mfaTotpDisableProc)))
|
||||
r.Get(tbp(l.MfaTotpNewSecret), h.handle(partAuthOnly(h.mfaTotpConfigForm)))
|
||||
r.Post(tbp(l.MfaTotpNewSecret), h.handle(partAuthOnly(h.mfaTotpConfigProc)))
|
||||
r.Get(tbp(l.MfaTotpQRImage), h.handle(partAuthOnly(h.mfaTotpConfigQR)))
|
||||
r.Get(tbp(l.MfaTotpDisable), h.handle(authOnly(h.mfaTotpDisableForm)))
|
||||
r.Post(tbp(l.MfaTotpDisable), h.handle(authOnly(h.mfaTotpDisableProc)))
|
||||
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
// OAuth2 routes
|
||||
r.HandleFunc(l.OAuth2Authorize, h.handle(h.oauth2Authorize))
|
||||
r.Get(l.OAuth2AuthorizeClient, h.handle(authOnly(h.oauth2AuthorizeClient)))
|
||||
r.Post(l.OAuth2AuthorizeClient, h.handle(authOnly(h.oauth2AuthorizeClientProc)))
|
||||
r.Get(l.OAuth2DefaultClient, h.handle(h.oauth2authorizeDefaultClient))
|
||||
r.Post(l.OAuth2DefaultClient, h.handle(h.oauth2authorizeDefaultClientProc))
|
||||
r.HandleFunc(tbp(l.OAuth2Authorize), h.handle(h.oauth2Authorize))
|
||||
r.Get(tbp(l.OAuth2AuthorizeClient), h.handle(authOnly(h.oauth2AuthorizeClient)))
|
||||
r.Post(tbp(l.OAuth2AuthorizeClient), h.handle(authOnly(h.oauth2AuthorizeClientProc)))
|
||||
r.Get(tbp(l.OAuth2DefaultClient), h.handle(h.oauth2authorizeDefaultClient))
|
||||
r.Post(tbp(l.OAuth2DefaultClient), h.handle(h.oauth2authorizeDefaultClientProc))
|
||||
})
|
||||
|
||||
r.Route(l.External+"/{provider}", func(r chi.Router) {
|
||||
r.Route(tbp(l.External)+"/{provider}", func(r chi.Router) {
|
||||
// External provider
|
||||
r.Get("/", h.externalInit)
|
||||
r.Get("/callback", h.externalCallback)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/99designs/basicauth-go"
|
||||
"net/http"
|
||||
|
||||
"github.com/766b/chi-prometheus"
|
||||
"github.com/99designs/basicauth-go"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -15,10 +15,10 @@ func metricsMiddleware(name string) func(http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func metricsMount(r chi.Router, username, password string) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Route("/metrics", func(r chi.Router) {
|
||||
r.Use(basicauth.New("Metrics", map[string][]string{
|
||||
username: {password},
|
||||
}))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.Handle("/", promhttp.Handler())
|
||||
})
|
||||
}
|
||||
|
||||
+74
-40
@@ -12,6 +12,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -42,6 +44,8 @@ func (s *server) MountRoutes(mm ...func(chi.Router)) {
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -53,14 +57,23 @@ func (s server) Serve(ctx context.Context) {
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
// Base middleware, CORS, RealIP, RequestID, context-logger
|
||||
router.Use(BaseMiddleware(s.environmentOpt.IsProduction(), s.log)...)
|
||||
router.Route("/"+strings.TrimPrefix(s.httpOpt.BaseUrl, "/"), func(r chi.Router) {
|
||||
// Reports error to Sentry if enabled
|
||||
if s.httpOpt.EnablePanicReporting {
|
||||
r.Use(sentryMiddleware())
|
||||
}
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
s.bindMiscRoutes(r)
|
||||
})
|
||||
if s.httpOpt.EnableMetrics {
|
||||
// Metrics tracking middleware
|
||||
r.Use(metricsMiddleware(s.httpOpt.MetricsServiceLabel))
|
||||
}
|
||||
|
||||
// Handle panic (sets 500 server error headers)
|
||||
r.Use(handlePanic)
|
||||
|
||||
// Base middleware, CORS, RealIP, RequestID, context-logger
|
||||
r.Use(BaseMiddleware(s.environmentOpt.IsProduction(), s.log)...)
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
// Logging request if enabled
|
||||
if s.httpOpt.LogRequest {
|
||||
r.Use(LogRequest)
|
||||
@@ -79,8 +92,26 @@ func (s server) Serve(ctx context.Context) {
|
||||
for _, mountRoutes := range s.endpoints {
|
||||
mountRoutes(r)
|
||||
}
|
||||
|
||||
if s.httpOpt.EnableMetrics {
|
||||
metricsMount(r, s.httpOpt.MetricsUsername, s.httpOpt.MetricsPassword)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if s.httpOpt.BaseUrl != "" {
|
||||
router.Handle("/", http.RedirectHandler(s.httpOpt.BaseUrl, http.StatusTemporaryRedirect))
|
||||
|
||||
}
|
||||
|
||||
s.bindMiscRoutes(router)
|
||||
|
||||
if s.httpOpt.EnableDebugRoute {
|
||||
router.Get("/__routes", debugRoutes(router))
|
||||
router.Get(s.httpOpt.BaseUrl+"/__routes", debugRoutes(router))
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
srv := http.Server{
|
||||
Handler: router,
|
||||
@@ -102,48 +133,51 @@ func (s server) Serve(ctx context.Context) {
|
||||
s.log.Info("HTTP server stopped", zap.Error(err))
|
||||
}
|
||||
|
||||
func (s server) bindMiscRoutes(router chi.Router) {
|
||||
if s.httpOpt.EnableMetrics {
|
||||
metricsMount(router, s.httpOpt.MetricsUsername, s.httpOpt.MetricsPassword)
|
||||
}
|
||||
|
||||
// Metrics tracking middleware
|
||||
if s.httpOpt.EnableMetrics {
|
||||
router.Use(metricsMiddleware(s.httpOpt.MetricsServiceLabel))
|
||||
}
|
||||
|
||||
// Handle panic (sets 500 server error headers)
|
||||
router.Use(handlePanic)
|
||||
|
||||
// Reports error to Sentry if enabled
|
||||
if s.httpOpt.EnablePanicReporting {
|
||||
router.Use(sentryMiddleware())
|
||||
}
|
||||
|
||||
func (s server) bindMiscRoutes(r chi.Router) {
|
||||
if s.httpOpt.EnableDebugRoute {
|
||||
s.log.Debug("profiler: /__profiler")
|
||||
router.Mount("/__profiler", middleware.Profiler())
|
||||
s.log.Debug("profiler enabled: /__profiler")
|
||||
r.Mount("/__profiler", middleware.Profiler())
|
||||
|
||||
s.log.Debug("list of routes: /__routes")
|
||||
router.Get("/__routes", debugRoutes(router))
|
||||
s.log.Debug("eventbus handlers debug enabled: /__eventbus")
|
||||
r.Get("/__eventbus", debugEventbus())
|
||||
|
||||
s.log.Debug("eventbus handlers: /__eventbus")
|
||||
router.Get("/__eventbus", debugEventbus())
|
||||
|
||||
s.log.Debug("corredor service: /__corredor")
|
||||
router.Get("/__corredor", debugCorredor())
|
||||
s.log.Debug("corredor service debug enabled: /__corredor")
|
||||
r.Get("/__corredor", debugCorredor())
|
||||
}
|
||||
|
||||
if s.httpOpt.EnableVersionRoute {
|
||||
router.Get("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||
api.Send(w, r, struct {
|
||||
BuildTime string `json:"buildTime"`
|
||||
Version string `json:"version"`
|
||||
}{version.BuildTime, version.Version})
|
||||
})
|
||||
var (
|
||||
dPath = "/version"
|
||||
sPath = path.Join(s.httpOpt.BaseUrl, dPath)
|
||||
v = func(w http.ResponseWriter, r *http.Request) {
|
||||
api.Send(w, r, struct {
|
||||
BuildTime string `json:"buildTime"`
|
||||
Version string `json:"version"`
|
||||
}{version.BuildTime, version.Version})
|
||||
}
|
||||
)
|
||||
|
||||
r.Get(dPath, v)
|
||||
if dPath != sPath {
|
||||
r.Get(sPath, v)
|
||||
}
|
||||
}
|
||||
|
||||
if s.httpOpt.EnableHealthcheckRoute {
|
||||
router.Get("/healthcheck", healthcheck.HttpHandler())
|
||||
// default & sub path for healthcheck endpoint
|
||||
var (
|
||||
dPath = "/healthcheck"
|
||||
sPath = path.Join(s.httpOpt.BaseUrl, dPath)
|
||||
log = s.log.With(zap.String("url", dPath))
|
||||
)
|
||||
|
||||
r.Get(dPath, healthcheck.HttpHandler())
|
||||
|
||||
if dPath != sPath {
|
||||
r.Get(sPath, healthcheck.HttpHandler())
|
||||
log = log.With(zap.String("url", sPath))
|
||||
}
|
||||
|
||||
log.Info("healthcheck endpoint enabled")
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+5
@@ -26,12 +26,14 @@ type (
|
||||
MetricsUsername string `env:"HTTP_METRICS_USERNAME"`
|
||||
MetricsPassword string `env:"HTTP_METRICS_PASSWORD"`
|
||||
EnablePanicReporting bool `env:"HTTP_REPORT_PANIC"`
|
||||
BaseUrl string `env:"HTTP_BASE_URL"`
|
||||
ApiEnabled bool `env:"HTTP_API_ENABLED"`
|
||||
ApiBaseUrl string `env:"HTTP_API_BASE_URL"`
|
||||
WebappEnabled bool `env:"HTTP_WEBAPP_ENABLED"`
|
||||
WebappBaseUrl string `env:"HTTP_WEBAPP_BASE_URL"`
|
||||
WebappBaseDir string `env:"HTTP_WEBAPP_BASE_DIR"`
|
||||
WebappList string `env:"HTTP_WEBAPP_LIST"`
|
||||
SslTerminated bool `env:"HTTP_SSL_TERMINATED"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -50,11 +52,14 @@ func HTTPServer() (o *HTTPServerOpt) {
|
||||
MetricsUsername: "metrics",
|
||||
MetricsPassword: string(rand.Bytes(5)),
|
||||
EnablePanicReporting: true,
|
||||
BaseUrl: "/",
|
||||
ApiEnabled: true,
|
||||
ApiBaseUrl: "/",
|
||||
WebappEnabled: false,
|
||||
WebappBaseUrl: "/",
|
||||
WebappBaseDir: "webapp/public",
|
||||
WebappList: "admin,compose,workflow",
|
||||
SslTerminated: isSecure(),
|
||||
}
|
||||
|
||||
fill(o)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package options
|
||||
|
||||
func (o *HTTPServerOpt) Defaults() {
|
||||
o.BaseUrl = CleanBase(o.BaseUrl)
|
||||
o.ApiBaseUrl = CleanBase(o.ApiBaseUrl)
|
||||
o.WebappBaseUrl = CleanBase(o.WebappBaseUrl)
|
||||
|
||||
if o.WebappEnabled && o.ApiEnabled && o.ApiBaseUrl == "" {
|
||||
if o.WebappEnabled && o.ApiEnabled && (o.ApiBaseUrl == "/" || o.ApiBaseUrl == "") {
|
||||
// api base URL is still on root (empty string)
|
||||
// but webapps are enabled (that means, server also serves static files from WebappBaseDir)
|
||||
//
|
||||
// Let's be nice and move API to /api
|
||||
o.ApiBaseUrl = "/api"
|
||||
o.ApiBaseUrl = CleanBase("api")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ props:
|
||||
env: HTTP_ERROR_TRACING
|
||||
default: false
|
||||
|
||||
|
||||
- name: enableHealthcheckRoute
|
||||
type: bool
|
||||
env: HTTP_ENABLE_HEALTHCHECK_ROUTE
|
||||
@@ -72,6 +71,12 @@ props:
|
||||
default: true
|
||||
description: Report HTTP panic to Sentry.
|
||||
|
||||
- name: baseUrl
|
||||
env: HTTP_BASE_URL
|
||||
default: "/"
|
||||
description: |-
|
||||
Base URL (prefix) for all routes (<baseUrl>/auth, <baseUrl>/api, ...)
|
||||
|
||||
- name: apiEnabled
|
||||
type: bool
|
||||
env: HTTP_API_ENABLED
|
||||
@@ -79,6 +84,11 @@ props:
|
||||
|
||||
- name: apiBaseUrl
|
||||
env: HTTP_API_BASE_URL
|
||||
default: "/"
|
||||
description: |-
|
||||
When webapps are enabled (HTTP_WEBAPP_ENABLED) this is moved to '/api' if not explicitly set otherwise.
|
||||
API base URL is prefixed with baseUrl
|
||||
|
||||
|
||||
- name: webappEnabled
|
||||
type: bool
|
||||
@@ -88,6 +98,8 @@ props:
|
||||
- name: webappBaseUrl
|
||||
env: HTTP_WEBAPP_BASE_URL
|
||||
default: "/"
|
||||
description: |-
|
||||
Webapp base URL is prefixed with baseUrl
|
||||
|
||||
- name: webappBaseDir
|
||||
env: HTTP_WEBAPP_BASE_DIR
|
||||
@@ -96,3 +108,12 @@ props:
|
||||
- name: webappList
|
||||
env: HTTP_WEBAPP_LIST
|
||||
default: "admin,compose,workflow"
|
||||
|
||||
- name: sslTerminated
|
||||
env: HTTP_SSL_TERMINATED
|
||||
type: bool
|
||||
default: isSecure()
|
||||
description: |-
|
||||
Is SSL termination enabled in ingres, proxy or load balancer that is in front of Corteza?
|
||||
By default, Corteza checks for presence of LETSENCRYPT_HOST environmental variable.
|
||||
This DOES NOT enable SSL termination in Cortreza!
|
||||
|
||||
Generated
+4
-5
@@ -9,7 +9,6 @@ package options
|
||||
// pkg/options/auth.yaml
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -44,13 +43,13 @@ func Auth() (o *AuthOpt) {
|
||||
o = &AuthOpt{
|
||||
Secret: getSecretFromEnv("jwt secret"),
|
||||
Expiry: time.Hour * 24 * 30,
|
||||
ExternalRedirectURL: guessBaseURL() + "/auth/external/{provider}/callback",
|
||||
ExternalRedirectURL: fullURL() + "/auth/external/{provider}/callback",
|
||||
ExternalCookieSecret: getSecretFromEnv("external cookie secret"),
|
||||
BaseURL: guessBaseURL() + "/auth",
|
||||
BaseURL: fullURL() + "/auth",
|
||||
SessionCookieName: "session",
|
||||
SessionCookiePath: "/auth",
|
||||
SessionCookiePath: pathPrefix() + "/auth",
|
||||
SessionCookieDomain: guessHostname(),
|
||||
SessionCookieSecure: strings.HasPrefix(guessBaseURL(), "https://"),
|
||||
SessionCookieSecure: isSecure(),
|
||||
SessionLifetime: 24 * time.Hour,
|
||||
SessionPermLifetime: 360 * 24 * time.Hour,
|
||||
GarbageCollectorInterval: 15 * time.Minute,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
imports:
|
||||
- time
|
||||
- strings
|
||||
|
||||
docs:
|
||||
title: Authentication
|
||||
@@ -50,7 +49,8 @@ props:
|
||||
- name: baseURL
|
||||
default: guessBaseURL() + "/auth"
|
||||
description: |-
|
||||
Frontend base URL. Must be an absolute URL
|
||||
Frontend base URL. Must be an absolute URL, with the domain.
|
||||
This is used for some redirects and links in auth emails.
|
||||
|
||||
- name: sessionCookieName
|
||||
default: "session"
|
||||
@@ -58,7 +58,7 @@ props:
|
||||
Session cookie name
|
||||
|
||||
- name: sessionCookiePath
|
||||
default: "/auth"
|
||||
default: pathPrefix() + "/auth"
|
||||
description: |-
|
||||
Session cookie path
|
||||
|
||||
@@ -69,9 +69,9 @@ props:
|
||||
|
||||
- name: sessionCookieSecure
|
||||
type: bool
|
||||
default: strings.HasPrefix(guessBaseURL(), "https://")
|
||||
default: isSecure()
|
||||
description: |-
|
||||
Defaults to true when HTTPS is used.
|
||||
Defaults to true when HTTPS is used. Corteza will try to guess the this setting by
|
||||
|
||||
- name: sessionLifetime
|
||||
type: time.Duration
|
||||
|
||||
+39
-6
@@ -2,6 +2,7 @@ package options
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -80,17 +81,49 @@ func guessHostname() string {
|
||||
return "local.cortezaproject.org"
|
||||
}
|
||||
|
||||
func guessBaseURL() string {
|
||||
// returns path prefix
|
||||
func pathPrefix() string {
|
||||
return CleanBase(EnvString("HTTP_BASE_URL", ""))
|
||||
}
|
||||
|
||||
// will return base URL with domain and prefix path
|
||||
func fullURL() string {
|
||||
var (
|
||||
host = guessHostname()
|
||||
_, isSecure = os.LookupEnv("LETSENCRYPT_HOST")
|
||||
full string
|
||||
host = guessHostname()
|
||||
)
|
||||
|
||||
if strings.Contains(host, "local.") || strings.Contains(host, "localhost") || !isSecure {
|
||||
return "http://" + host
|
||||
if strings.Contains(host, "local.") || strings.Contains(host, "localhost") || !isSecure() {
|
||||
full = "http://" + host
|
||||
} else {
|
||||
return "https://" + host
|
||||
full = "https://" + host
|
||||
}
|
||||
|
||||
if pp := pathPrefix(); pp != "" {
|
||||
return full + pp
|
||||
}
|
||||
|
||||
return full
|
||||
}
|
||||
|
||||
// Checks value of HTTP_SSL_TERMINATED and fallbacks to checking existence of LETSENCRYPT_HOST
|
||||
// to determinate we are behind a SSL termination infrastructure
|
||||
func isSecure() bool {
|
||||
var _, is = os.LookupEnv("LETSENCRYPT_HOST")
|
||||
|
||||
// We're accessing to HTTP_SSL_TERMINATED directly here and not via HttpServerOpt.SslTerminated
|
||||
// because we need to get to this value early and directly
|
||||
return EnvBool("HTTP_SSL_TERMINATED", is)
|
||||
}
|
||||
|
||||
// Path joins all parts with / and prefixes result (if not empty) with /
|
||||
func CleanBase(pp ...string) string {
|
||||
if p := strings.Trim(path.Join(pp...), "/"); p != "" {
|
||||
// prefix base with slash
|
||||
return "/" + p
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func EnvString(key string, def string) string {
|
||||
|
||||
+95
-31
@@ -1,60 +1,95 @@
|
||||
package webapp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
"github.com/go-chi/chi"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MakeWebappServer(httpSrvOpt options.HTTPServerOpt, authOpt options.AuthOpt, fed options.FederationOpt) func(r chi.Router) {
|
||||
// Serves static files directly from FS
|
||||
return func(r chi.Router) {
|
||||
fileserver := http.FileServer(http.Dir(httpSrvOpt.WebappBaseDir))
|
||||
var (
|
||||
htmlIndex struct {
|
||||
body []byte
|
||||
mod time.Time
|
||||
}
|
||||
)
|
||||
|
||||
for _, app := range strings.Split(httpSrvOpt.WebappList, ",") {
|
||||
basedir := path.Join(httpSrvOpt.WebappBaseUrl, app)
|
||||
serveConfig(r, basedir, httpSrvOpt.ApiBaseUrl, authOpt.BaseURL, fed.Enabled)
|
||||
r.Get(basedir+"*", serveIndex(httpSrvOpt.WebappBaseDir, basedir, fileserver))
|
||||
func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HTTPServerOpt, authOpt options.AuthOpt) func(r chi.Router) {
|
||||
var (
|
||||
apiBaseUrl = options.CleanBase(httpSrvOpt.BaseUrl, httpSrvOpt.ApiBaseUrl)
|
||||
apps = strings.Split(httpSrvOpt.WebappList, ",")
|
||||
|
||||
appIndexHTMLs = make(map[string][]byte)
|
||||
|
||||
webBaseUrl string
|
||||
err error
|
||||
)
|
||||
|
||||
// Preload index files for all apps
|
||||
for _, app := range append(apps, "") {
|
||||
pathPrefix := path.Join(httpSrvOpt.BaseUrl, httpSrvOpt.WebappBaseUrl, app)
|
||||
if !strings.HasSuffix(pathPrefix, "/") {
|
||||
pathPrefix += "/"
|
||||
}
|
||||
|
||||
serveConfig(r, httpSrvOpt.WebappBaseUrl, httpSrvOpt.ApiBaseUrl, authOpt.BaseURL, fed.Enabled)
|
||||
r.Get(httpSrvOpt.WebappBaseUrl+"*", serveIndex(httpSrvOpt.WebappBaseDir, httpSrvOpt.WebappBaseUrl, fileserver))
|
||||
appIndexHTMLs[app], err = modifyIndexHTML(path.Join(httpSrvOpt.WebappBaseDir, app), pathPrefix)
|
||||
if err != nil {
|
||||
log.Error("could not preload application index HTML", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Serves static files directly from FS
|
||||
return func(r chi.Router) {
|
||||
fileserver := http.StripPrefix(
|
||||
path.Join(httpSrvOpt.BaseUrl, httpSrvOpt.WebappBaseUrl),
|
||||
http.FileServer(http.Dir(httpSrvOpt.WebappBaseDir)),
|
||||
)
|
||||
|
||||
for _, app := range apps {
|
||||
webBaseUrl = "/" + path.Join(httpSrvOpt.WebappBaseUrl, app)
|
||||
serveConfig(r, webBaseUrl, apiBaseUrl, authOpt.BaseURL, httpSrvOpt.BaseUrl)
|
||||
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[app], fileserver))
|
||||
}
|
||||
|
||||
webBaseUrl = "/" + path.Join(httpSrvOpt.WebappBaseUrl)
|
||||
serveConfig(r, webBaseUrl, apiBaseUrl, authOpt.BaseURL, httpSrvOpt.BaseUrl)
|
||||
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[""], fileserver))
|
||||
}
|
||||
}
|
||||
|
||||
// Serves index.html in case the requested file isn't found (or some other os.Stat error)
|
||||
func serveIndex(assetPath string, indexPath string, serve http.Handler) http.HandlerFunc {
|
||||
func serveIndex(opt options.HTTPServerOpt, indexHTML []byte, serve http.Handler) http.HandlerFunc {
|
||||
//indexPage := path.Join(opt.WebappBaseDir, indexPath, "index.html")
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
indexPage := path.Join(assetPath, indexPath, "index.html")
|
||||
requestedPage := path.Join(assetPath, r.URL.Path)
|
||||
|
||||
_, err := os.Stat(requestedPage)
|
||||
|
||||
if err == nil {
|
||||
if strings.HasSuffix(r.URL.String(), "/") {
|
||||
// If request ends with a slash we want to prevent
|
||||
// serving of directory index (list of fileS)
|
||||
http.ServeFile(w, r, indexPage)
|
||||
return
|
||||
}
|
||||
requestedFile := path.Join(opt.WebappBaseDir, strings.TrimPrefix(r.URL.Path, opt.BaseUrl))
|
||||
|
||||
f, err := os.Stat(requestedFile)
|
||||
// When file does not exist or is a directory, serve app's index
|
||||
if os.IsNotExist(err) || f.IsDir() || strings.HasSuffix(r.URL.String(), "/") {
|
||||
// Make sure index is not cached
|
||||
// In the big scheme of things, this couple of kilobytes does not make any difference
|
||||
// and it's really important that users get fresh index files on each full refresh
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(indexHTML)
|
||||
return
|
||||
} else if err == nil {
|
||||
// Serve the file requested
|
||||
serve.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
// Forcefully serve index page on whatever
|
||||
http.ServeFile(w, r, indexPage)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Default().WithOptions(zap.AddStacktrace(zap.PanicLevel)).Error(
|
||||
"failed to serve static file",
|
||||
zap.Error(err),
|
||||
@@ -65,10 +100,39 @@ func serveIndex(assetPath string, indexPath string, serve http.Handler) http.Han
|
||||
}
|
||||
}
|
||||
|
||||
func serveConfig(r chi.Router, appUrl, apiBaseUrl, authBaseUrl string, fedEnabled bool) {
|
||||
r.Get(strings.TrimRight(appUrl, "/")+"/config.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
func serveConfig(r chi.Router, appUrl, apiBaseUrl, authBaseUrl, webappBaseUrl string) {
|
||||
r.Get(strings.TrimSuffix(appUrl, "/")+"/config.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
const line = "window.%s = '%s';\n"
|
||||
_, _ = fmt.Fprintf(w, line, "CortezaAPI", apiBaseUrl)
|
||||
_, _ = fmt.Fprintf(w, line, "CortezaAuth", authBaseUrl)
|
||||
_, _ = fmt.Fprintf(w, line, "CortezaWebapp", webappBaseUrl)
|
||||
})
|
||||
}
|
||||
|
||||
// Reads and modifies index HTML for the webapp
|
||||
//
|
||||
// It replaces <base> tag with the exact location of the web app
|
||||
func modifyIndexHTML(dir, baseHref string) (buf []byte, err error) {
|
||||
var (
|
||||
warning = []byte("\n\n<!--\n\nError!\n\nFailed could not locate or modify <base> tag, your webapp might misbehave\n\n-->\n")
|
||||
placeholder = []byte("<base href=/ >")
|
||||
replacement = []byte(fmt.Sprintf(`<base href="%s" />`, baseHref))
|
||||
fh *os.File
|
||||
)
|
||||
|
||||
fh, err = os.Open(path.Join(dir, "index.html"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if bytes.Contains(buf, placeholder) {
|
||||
return bytes.Replace(buf, placeholder, replacement, 1), nil
|
||||
} else {
|
||||
return append(buf, warning...), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,60 +4,60 @@ applications:
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/low-code-platform.png
|
||||
logo: /applications/low-code-platform.png
|
||||
url: /compose
|
||||
icon: applications/low-code-platform.png
|
||||
logo: applications/low-code-platform.png
|
||||
url: compose/
|
||||
|
||||
- name: CRM Suite
|
||||
(envoy): { skipIf: "!missing" }
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/low-code-crm-app.png
|
||||
logo: /applications/low-code-crm-app.png
|
||||
url: /compose/ns/crm/pages
|
||||
icon: applications/low-code-crm-app.png
|
||||
logo: applications/low-code-crm-app.png
|
||||
url: compose/ns/crm/pages
|
||||
|
||||
- name: Service Solution
|
||||
(envoy): { skipIf: "!missing" }
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/low-code-service-solution-app.png
|
||||
logo: /applications/low-code-service-solution-app.png
|
||||
url: /compose/ns/service-solution/pages
|
||||
icon: applications/low-code-service-solution-app.png
|
||||
logo: applications/low-code-service-solution-app.png
|
||||
url: compose/ns/service-solution/pages
|
||||
|
||||
- name: Jitsi Bridge
|
||||
(envoy): { skipIf: "!missing" }
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/video-conference.png
|
||||
logo: /applications/video-conference.png
|
||||
url: /bridge/jitsi/
|
||||
icon: applications/video-conference.png
|
||||
logo: applications/video-conference.png
|
||||
url: bridge/jitsi/
|
||||
|
||||
- name: Google Maps
|
||||
(envoy): { skipIf: "!missing" }
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/google-maps.png
|
||||
logo: /applications/google-maps.png
|
||||
url: /bridge/google-maps/
|
||||
icon: applications/google-maps.png
|
||||
logo: applications/google-maps.png
|
||||
url: bridge/google-maps/
|
||||
|
||||
- name: Admin Area
|
||||
(envoy): { skipIf: "!missing" }
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/admin-area.png
|
||||
logo: /applications/admin-area.png
|
||||
url: /admin
|
||||
icon: applications/admin-area.png
|
||||
logo: applications/admin-area.png
|
||||
url: admin/
|
||||
|
||||
- name: Workflows
|
||||
(envoy): { skipIf: "!missing" }
|
||||
enabled: true
|
||||
unify:
|
||||
listed: true
|
||||
icon: /applications/workflows.png
|
||||
logo: /applications/workflows.png
|
||||
url: /workflow
|
||||
icon: applications/workflows.png
|
||||
logo: applications/workflows.png
|
||||
url: workflow/
|
||||
|
||||
+32
-10
@@ -1,3 +1,5 @@
|
||||
.PHONY: all install-fresh install-packages build clean cleaner
|
||||
|
||||
WGET ?= wget
|
||||
WGET_FLAGS ?= -q
|
||||
TAR ?= tar
|
||||
@@ -7,21 +9,41 @@ APPS ?= admin compose workflow
|
||||
ALL_APPS ?= $(APPS) one
|
||||
VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
PACKAGES = $(addprefix corteza-webapp-,$(addsuffix -$(VERSION).tar.gz,$(ALL_APPS)))
|
||||
SOURCE_LOC ?= "../../webapp-"
|
||||
SOURCES = $(addprefix $(SOURCE_LOC),$(ALL_APPS))
|
||||
|
||||
all: install
|
||||
all: install-packages
|
||||
|
||||
install: $(PACKAGES)
|
||||
rm -rf $(addprefix public/,$(APPS))
|
||||
mkdir -p $(addprefix public/,$(APPS))
|
||||
$(TAR) $(TAR_FLAGS) -f corteza-webapp-one-$(VERSION).tar.gz -C public
|
||||
$(TAR) $(TAR_FLAGS) -f corteza-webapp-admin-$(VERSION).tar.gz -C public/admin
|
||||
$(TAR) $(TAR_FLAGS) -f corteza-webapp-compose-$(VERSION).tar.gz -C public/compose
|
||||
$(TAR) $(TAR_FLAGS) -f corteza-webapp-workflow-$(VERSION).tar.gz -C public/workflow
|
||||
install-packages: $(PACKAGES)
|
||||
$(info installing packages to public/)
|
||||
@ rm -rf $(addprefix public/,$(APPS))
|
||||
@ mkdir -p $(addprefix public/,$(APPS))
|
||||
@ $(TAR) $(TAR_FLAGS) -f corteza-webapp-one-$(VERSION).tar.gz -C public
|
||||
@ $(TAR) $(TAR_FLAGS) -f corteza-webapp-admin-$(VERSION).tar.gz -C public/admin
|
||||
@ $(TAR) $(TAR_FLAGS) -f corteza-webapp-compose-$(VERSION).tar.gz -C public/compose
|
||||
@ $(TAR) $(TAR_FLAGS) -f corteza-webapp-workflow-$(VERSION).tar.gz -C public/workflow
|
||||
|
||||
download: $(PACKAGES)
|
||||
|
||||
$(PACKAGES):
|
||||
$(WGET) $(WGET_FLAGS) $(RELEASE_PAGE)/$(@)
|
||||
$(info downloading $(RELEASE_PAGE)/$(@))
|
||||
@ $(WGET) $(WGET_FLAGS) $(RELEASE_PAGE)/$(@)
|
||||
|
||||
install-fresh: build
|
||||
@ rm -rf public
|
||||
@ cp -r $(SOURCE_LOC)one/dist public/
|
||||
@ cp -r $(SOURCE_LOC)admin/dist public/admin
|
||||
@ cp -r $(SOURCE_LOC)compose/dist public/compose
|
||||
@ cp -r $(SOURCE_LOC)workflow/dist public/workflow
|
||||
|
||||
build: $(SOURCES)
|
||||
|
||||
$(SOURCES):
|
||||
$(info building $(@))
|
||||
@ cd $(@) && yarn build
|
||||
|
||||
clean:
|
||||
rm -f $(PACKAGES)
|
||||
@ rm -f $(PACKAGES)
|
||||
|
||||
cleaner:
|
||||
@ rm -rf public corteza-webapp-*.tar.gz
|
||||
|
||||
+40
-2
@@ -1,14 +1,52 @@
|
||||
= Web applications
|
||||
|
||||
Rationale behind `/webapp/...` tools is to allow backend developers to quicky test server with frontend applications.
|
||||
Rationale behind `/webapp/...` tools is to allow backend developers to quickly test server with frontend applications.
|
||||
Tools (see Makefile) download and install unstable (can be changed with `VERSION` var).
|
||||
|
||||
.Downloads and installs unstable packages under webapp/public
|
||||
.Downloads and installs packages under webapp/public using current version
|
||||
[source,shell]
|
||||
----
|
||||
make
|
||||
----
|
||||
|
||||
.Downloads and installs packages under webapp/public using specific version
|
||||
[source,shell]
|
||||
----
|
||||
make VERSION=2021.3.3
|
||||
----
|
||||
|
||||
. All available make targets
|
||||
[cols="2m,5a"]
|
||||
|===
|
||||
|target|
|
||||
|
||||
| all
|
||||
| default target alias for install-packages
|
||||
|
||||
| install-package
|
||||
| installs all packages
|
||||
|
||||
| download
|
||||
| downloads all packages
|
||||
|
||||
| install-fresh
|
||||
| builds and installs all packages from source
|
||||
|
||||
| install-fresh
|
||||
| builds and installs all web applications from source
|
||||
|
||||
| build
|
||||
| builds all web applications from source
|
||||
|
||||
| clean
|
||||
| removes packages of the current versions
|
||||
|
||||
| cleaner
|
||||
| removes all
|
||||
|
||||
|===
|
||||
|
||||
|
||||
To enable serving of webapps from the server, make sure `HTTP_WEBAPP_ENABLED` is set to `true`.
|
||||
This will prefix all API endpoints with `/api/`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user