From c57260349b5102fb08d36cb19d22a3c448d9aba3 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Mon, 31 Dec 2018 11:46:29 -0800 Subject: [PATCH 1/2] web + ui: Pass init cookie from server to client --- pkg/server/web/app_json.go | 5 +++++ pkg/server/web/handlers.go | 10 +++++++++ pkg/server/web/server.go | 21 ++++++++++++++++++- ui/src/Data/AppConfig.elm | 15 ++++++++++++++ ui/src/Data/Session.elm | 18 ++++++++++------ ui/src/Main.elm | 23 ++++++++++++++++++--- ui/src/index.js | 42 ++++++++++++++++++++++++++++++++++++-- 7 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 pkg/server/web/app_json.go create mode 100644 ui/src/Data/AppConfig.elm diff --git a/pkg/server/web/app_json.go b/pkg/server/web/app_json.go new file mode 100644 index 0000000..c80331c --- /dev/null +++ b/pkg/server/web/app_json.go @@ -0,0 +1,5 @@ +package web + +type jsonAppConfig struct { + MonitorVisible bool `json:"monitor-visible"` +} diff --git a/pkg/server/web/handlers.go b/pkg/server/web/handlers.go index feb9a5b..15c65ec 100644 --- a/pkg/server/web/handlers.go +++ b/pkg/server/web/handlers.go @@ -31,6 +31,16 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } +// cookieHandler injects an HTTP cookie into the response. +func cookieHandler(cookie *http.Cookie, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + log.Debug().Str("module", "web").Str("remote", req.RemoteAddr).Str("proto", req.Proto). + Str("method", req.Method).Str("path", req.RequestURI).Msg("Injecting cookie") + http.SetCookie(w, cookie) + next.ServeHTTP(w, req) + }) +} + // fileHandler creates a handler that sends the named file regardless of the requested URL. func fileHandler(name string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/server/web/server.go b/pkg/server/web/server.go index 957b7f7..d6e0b99 100644 --- a/pkg/server/web/server.go +++ b/pkg/server/web/server.go @@ -3,10 +3,12 @@ package web import ( "context" + "encoding/json" "expvar" "net" "net/http" "net/http/pprof" + "net/url" "path/filepath" "time" @@ -75,7 +77,8 @@ func Initialize( fileHandler(filepath.Join(conf.Web.UIDir, "favicon.png"))) // SPA managed paths. - spaHandler := fileHandler(filepath.Join(conf.Web.UIDir, "index.html")) + spaHandler := cookieHandler(appConfigCookie(conf.Web), + fileHandler(filepath.Join(conf.Web.UIDir, "index.html"))) Router.Path("/").Handler(spaHandler) Router.Path("/monitor").Handler(spaHandler) Router.Path("/status").Handler(spaHandler) @@ -126,6 +129,22 @@ func Start(ctx context.Context) { } } +func appConfigCookie(webConfig config.Web) *http.Cookie { + o := &jsonAppConfig{ + MonitorVisible: webConfig.MonitorVisible, + } + b, err := json.Marshal(o) + if err != nil { + log.Error().Str("module", "web").Str("phase", "startup").Err(err). + Msg("Failed to convert app-config to JSON") + } + return &http.Cookie{ + Name: "app-config", + Value: url.PathEscape(string(b)), + Path: "/", + } +} + // serve begins serving HTTP requests func serve(ctx context.Context) { // server.Serve blocks until we close the listener diff --git a/ui/src/Data/AppConfig.elm b/ui/src/Data/AppConfig.elm new file mode 100644 index 0000000..d86dbe9 --- /dev/null +++ b/ui/src/Data/AppConfig.elm @@ -0,0 +1,15 @@ +module Data.AppConfig exposing (AppConfig, decoder) + +import Json.Decode as D +import Json.Decode.Pipeline as P + + +type alias AppConfig = + { monitorVisible : Bool + } + + +decoder : D.Decoder AppConfig +decoder = + D.succeed AppConfig + |> P.required "monitor-visible" D.bool diff --git a/ui/src/Data/Session.elm b/ui/src/Data/Session.elm index 3821292..4e2d72b 100644 --- a/ui/src/Data/Session.elm +++ b/ui/src/Data/Session.elm @@ -4,11 +4,11 @@ module Data.Session exposing , Session , addRecent , clearFlash - , decodeValueWithDefault , decoder , disableRouting , enableRouting , init + , initError , showFlash ) @@ -54,6 +54,17 @@ init key location persistent = } +initError : Nav.Key -> Url -> String -> Session +initError key location error = + { key = key + , host = location.host + , flash = Just (Flash "Initialization failed" [ ( "Error", error ) ]) + , routing = True + , zone = Time.utc + , persistent = Persistent [] + } + + addRecent : String -> Session -> Session addRecent mailbox session = if List.head session.persistent.recentMailboxes == Just mailbox then @@ -99,11 +110,6 @@ decoder = |> optional "recentMailboxes" (D.list D.string) [] -decodeValueWithDefault : D.Value -> Persistent -decodeValueWithDefault = - D.decodeValue decoder >> Result.withDefault { recentMailboxes = [] } - - encode : Persistent -> E.Value encode persistent = E.object diff --git a/ui/src/Main.elm b/ui/src/Main.elm index 30c207a..7bf5b04 100644 --- a/ui/src/Main.elm +++ b/ui/src/Main.elm @@ -2,7 +2,8 @@ module Main exposing (main) import Browser exposing (Document, UrlRequest) import Browser.Navigation as Nav -import Data.Session as Session exposing (Session, decoder) +import Data.AppConfig as AppConfig exposing (AppConfig) +import Data.Session as Session exposing (Session) import Html exposing (..) import Json.Decode as D exposing (Value) import Page.Home as Home @@ -34,11 +35,27 @@ type alias Model = } +type alias InitConfig = + { appConfig : AppConfig + , session : Session.Persistent + } + + init : Value -> Url -> Nav.Key -> ( Model, Cmd Msg ) -init sessionValue location key = +init configValue location key = let + configDecoder = + D.map2 InitConfig + (D.field "app-config" AppConfig.decoder) + (D.field "session" Session.decoder) + session = - Session.init key location (Session.decodeValueWithDefault sessionValue) + case D.decodeValue configDecoder configValue of + Ok config -> + Session.init key location config.session + + Err error -> + Session.initError key location (D.errorToString error) ( subModel, _ ) = Home.init session diff --git a/ui/src/index.js b/ui/src/index.js index 6d5551e..85ded58 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -4,10 +4,16 @@ import { Elm } from './Main.elm' import registerMonitorPorts from './registerMonitor' import './renderedHtml' +// Initial configuration from Inbucket server to Elm App. +var flags = { + "app-config": appConfig(), + "session": sessionObject(), +} + // App startup. var app = Elm.Main.init({ node: document.getElementById('root'), - flags: sessionObject() + flags: flags, }) // Message monitor. @@ -24,9 +30,24 @@ window.addEventListener("storage", function (event) { } }, false) +// Decode the JSON value of the app-config cookie, then delete it. +function appConfig() { + var name = "app-config" + var c = getCookie(name) + if (c) { + deleteCookie(name) + return JSON.parse(decodeURIComponent(c)) + } + console.warn("Inbucket " + name + " cookie not found, running with defaults.") + return { + "monitor-visible": true, + } +} + +// Grab peristent session data out of local storage. function sessionObject() { - var s = localStorage.session try { + var s = localStorage.session if (s) { return JSON.parse(s) } @@ -35,3 +56,20 @@ function sessionObject() { } return null } + +function getCookie(cookieName) { + var name = cookieName + "=" + var cookies = decodeURIComponent(document.cookie).split(';') + for (var i=0; i Date: Mon, 31 Dec 2018 14:37:11 -0800 Subject: [PATCH 2/2] ui: Respect monitor visible config option --- etc/dev-start.sh | 1 + ui/src/Data/AppConfig.elm | 7 ++++++- ui/src/Data/Session.elm | 8 ++++++-- ui/src/Main.elm | 18 +++++++++++++++--- ui/src/Views/Page.elm | 6 +++++- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/etc/dev-start.sh b/etc/dev-start.sh index 60ddcb6..19d868e 100755 --- a/etc/dev-start.sh +++ b/etc/dev-start.sh @@ -12,6 +12,7 @@ export INBUCKET_SMTP_STOREDOMAINS="important.local" export INBUCKET_WEB_TEMPLATECACHE="false" export INBUCKET_WEB_COOKIEAUTHKEY="not-secret" export INBUCKET_WEB_UIDIR="ui/dist" +#export INBUCKET_WEB_MONITORVISIBLE="false" export INBUCKET_STORAGE_TYPE="file" export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket" export INBUCKET_STORAGE_RETENTIONPERIOD="3h" diff --git a/ui/src/Data/AppConfig.elm b/ui/src/Data/AppConfig.elm index d86dbe9..b18701f 100644 --- a/ui/src/Data/AppConfig.elm +++ b/ui/src/Data/AppConfig.elm @@ -1,4 +1,4 @@ -module Data.AppConfig exposing (AppConfig, decoder) +module Data.AppConfig exposing (AppConfig, decoder, default) import Json.Decode as D import Json.Decode.Pipeline as P @@ -13,3 +13,8 @@ decoder : D.Decoder AppConfig decoder = D.succeed AppConfig |> P.required "monitor-visible" D.bool + + +default : AppConfig +default = + AppConfig True diff --git a/ui/src/Data/Session.elm b/ui/src/Data/Session.elm index 4e2d72b..c90fe91 100644 --- a/ui/src/Data/Session.elm +++ b/ui/src/Data/Session.elm @@ -13,6 +13,7 @@ module Data.Session exposing ) import Browser.Navigation as Nav +import Data.AppConfig as AppConfig exposing (AppConfig) import Html exposing (Html) import Json.Decode as D import Json.Decode.Pipeline exposing (..) @@ -28,6 +29,7 @@ type alias Session = , flash : Maybe Flash , routing : Bool , zone : Time.Zone + , config : AppConfig , persistent : Persistent } @@ -43,13 +45,14 @@ type alias Persistent = } -init : Nav.Key -> Url -> Persistent -> Session -init key location persistent = +init : Nav.Key -> Url -> AppConfig -> Persistent -> Session +init key location config persistent = { key = key , host = location.host , flash = Nothing , routing = True , zone = Time.utc + , config = config , persistent = persistent } @@ -61,6 +64,7 @@ initError key location error = , flash = Just (Flash "Initialization failed" [ ( "Error", error ) ]) , routing = True , zone = Time.utc + , config = AppConfig.default , persistent = Persistent [] } diff --git a/ui/src/Main.elm b/ui/src/Main.elm index 7bf5b04..87d36cc 100644 --- a/ui/src/Main.elm +++ b/ui/src/Main.elm @@ -52,7 +52,7 @@ init configValue location key = session = case D.decodeValue configDecoder configValue of Ok config -> - Session.init key location config.session + Session.init key location config.appConfig config.session Err error -> Session.initError key location (D.errorToString error) @@ -255,8 +255,20 @@ changeRouteTo route model = |> updateWith Mailbox MailboxMsg model Route.Monitor -> - Monitor.init session - |> updateWith Monitor MonitorMsg model + if session.config.monitorVisible then + Monitor.init session + |> updateWith Monitor MonitorMsg model + + else + let + flash = + { title = "Unknown route requested" + , table = [ ( "Error", "Monitor disabled by configuration." ) ] + } + in + ( applyToModelSession (Session.showFlash flash) model + , Cmd.none + ) Route.Status -> Status.init session diff --git a/ui/src/Views/Page.elm b/ui/src/Views/Page.elm index 4a58cbe..4044e2f 100644 --- a/ui/src/Views/Page.elm +++ b/ui/src/Views/Page.elm @@ -44,7 +44,11 @@ frame controls session page modal content = [ ul [ class "navbar", attribute "role" "navigation" ] [ li [ class "navbar-brand" ] [ a [ Route.href Route.Home ] [ text "@ inbucket" ] ] - , navbarLink page Route.Monitor [ text "Monitor" ] + , if session.config.monitorVisible then + navbarLink page Route.Monitor [ text "Monitor" ] + + else + text "" , navbarLink page Route.Status [ text "Status" ] , navbarRecent page controls , li [ class "navbar-mailbox" ]