From 391dc51c13d4a11647f39a387885089c001c5feb Mon Sep 17 00:00:00 2001 From: Eric Chiang <eric.chiang@coreos.com> Date: Wed, 30 Nov 2016 14:26:54 -0800 Subject: [PATCH] *: add theme based frontend configuration This PR reworks the web layout so static files can be provided and a "themes" directory to allow a certain degree of control over logos, styles, etc. This PR does NOT add general support for frontend customization, only enough to allow us to start exploring theming internally. The dex binary also must now be run from the root directory since templates are no longer "compiled into" the binary. The docker image has been updated with frontend assets. --- Dockerfile | 5 + Makefile | 8 +- cmd/dex/config.go | 2 +- cmd/dex/serve.go | 2 +- examples/config-dev.yaml | 2 +- server/server.go | 45 +++- server/server_test.go | 5 + server/templates.go | 188 ++++++++++------- server/templates_default.go | 362 -------------------------------- server/templates_default_gen.go | 85 -------- server/templates_test.go | 15 -- web/static/main.css | 0 web/templates/header.html | 6 +- web/templates/login.html | 2 +- web/themes/coreos/logo.png | Bin 0 -> 2218 bytes web/themes/coreos/style.css | 0 16 files changed, 173 insertions(+), 554 deletions(-) delete mode 100644 server/templates_default.go delete mode 100644 server/templates_default_gen.go create mode 100644 web/static/main.css create mode 100644 web/themes/coreos/logo.png create mode 100644 web/themes/coreos/style.css diff --git a/Dockerfile b/Dockerfile index c5361667..4f0d43a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,11 @@ RUN apk add --update ca-certificates openssl COPY _output/bin/dex /usr/local/bin/dex +# Import frontend assets and set the correct CWD directory so the assets +# are in the default path. +COPY web /web +WORKDIR / + ENTRYPOINT ["dex"] CMD ["version"] diff --git a/Makefile b/Makefile index 62ab5895..5aa8144e 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ LD_FLAGS="-w -X $(REPO_PATH)/version.Version=$(VERSION)" build: bin/dex bin/example-app -bin/dex: FORCE generated +bin/dex: FORCE @go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex bin/example-app: FORCE @@ -35,9 +35,6 @@ bin/example-app: FORCE release-binary: @go build -o _output/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex -.PHONY: generated -generated: server/templates_default.go - test: @go test -v -i $(shell go list ./... | grep -v '/vendor/') @go test -v $(shell go list ./... | grep -v '/vendor/') @@ -57,9 +54,6 @@ lint: golint -set_exit_status $$package $$i || exit 1; \ done -server/templates_default.go: $(wildcard web/templates/**) - @go run server/templates_default_gen.go - _output/bin/dex: # Using rkt to build the dex binary. @./scripts/rkt-build diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 2ee9e58a..dc3715b8 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -30,7 +30,7 @@ type Config struct { GRPC GRPC `json:"grpc"` Expiry Expiry `json:"expiry"` - Templates server.TemplateConfig `json:"templates"` + Frontend server.WebConfig `json:"frontend"` // StaticClients cause the server to use this list of clients rather than // querying the storage. Write operations, like creating a client, will fail. diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index e218e473..c9baa9d3 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -151,7 +151,7 @@ func serve(cmd *cobra.Command, args []string) error { Issuer: c.Issuer, Connectors: connectors, Storage: s, - TemplateConfig: c.Templates, + Web: c.Frontend, EnablePasswordDB: c.EnablePasswordDB, } if c.Expiry.SigningKeys != "" { diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml index 5f94e202..1b5a48d3 100644 --- a/examples/config-dev.yaml +++ b/examples/config-dev.yaml @@ -14,7 +14,7 @@ storage: # Configuration for the HTTP endpoints. web: - http: 127.0.0.1:5556 + http: 0.0.0.0:5556 # Uncomment for HTTPS options. # https: 127.0.0.1:5554 # tlsCert: /etc/dex/tls.crt diff --git a/server/server.go b/server/server.go index b8d2c8d3..91d119e9 100644 --- a/server/server.go +++ b/server/server.go @@ -56,7 +56,32 @@ type Config struct { EnablePasswordDB bool - TemplateConfig TemplateConfig + Web WebConfig +} + +// WebConfig holds the server's frontend templates and asset configuration. +// +// These are currently very custom to CoreOS and it's not recommended that +// outside users attempt to customize these. +type WebConfig struct { + // A filepath to web static. + // + // It is expected to contain the following directories: + // + // * static - Static static served at "( issuer URL )/static". + // * templates - HTML templates controlled by dex. + // * themes/(theme) - Static static served at "( issuer URL )/theme". + // + Dir string + + // Defaults to "( issuer URL )/theme/logo.png" + LogoURL string + + // Defaults to "dex" + Issuer string + + // Defaults to "coreos" + Theme string } func value(val, defaultValue time.Duration) time.Duration { @@ -130,9 +155,17 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) supported[respType] = true } - tmpls, err := loadTemplates(c.TemplateConfig) + web := webConfig{ + dir: c.Web.Dir, + logoURL: c.Web.LogoURL, + issuerURL: c.Issuer, + issuer: c.Web.Issuer, + theme: c.Web.Theme, + } + + static, theme, tmpls, err := loadWebConfig(web) if err != nil { - return nil, fmt.Errorf("server: failed to load templates: %v", err) + return nil, fmt.Errorf("server: failed to load web static: %v", err) } now := c.Now @@ -159,6 +192,10 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) handleFunc := func(p string, h http.HandlerFunc) { r.HandleFunc(path.Join(issuerURL.Path, p), h) } + handlePrefix := func(p string, h http.Handler) { + prefix := path.Join(issuerURL.Path, p) + r.PathPrefix(prefix).Handler(http.StripPrefix(prefix, h)) + } r.NotFoundHandler = http.HandlerFunc(s.notFound) discoveryHandler, err := s.discoveryHandler() @@ -175,6 +212,8 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) handleFunc("/callback", s.handleConnectorCallback) handleFunc("/approval", s.handleApproval) handleFunc("/healthz", s.handleHealth) + handlePrefix("/static", static) + handlePrefix("/theme", theme) s.mux = r startKeyRotation(ctx, c.Storage, rotationStrategy, now) diff --git a/server/server_test.go b/server/server_test.go index a5865dfa..3ab41940 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -11,6 +11,8 @@ import ( "net/http/httptest" "net/http/httputil" "net/url" + "os" + "path/filepath" "reflect" "sort" "strings" @@ -85,6 +87,9 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi Connector: mock.NewCallbackConnector(), }, }, + Web: WebConfig{ + Dir: filepath.Join(os.Getenv("GOPATH"), "src/github.com/coreos/dex/web"), + }, } if updateConfig != nil { updateConfig(&config) diff --git a/server/templates.go b/server/templates.go index e8285fe3..649d5b08 100644 --- a/server/templates.go +++ b/server/templates.go @@ -6,8 +6,10 @@ import ( "io/ioutil" "log" "net/http" + "os" "path/filepath" "sort" + "strings" "text/template" ) @@ -18,8 +20,6 @@ const ( tmplOOB = "oob.html" ) -const coreOSLogoURL = "https://coreos.com/assets/images/brand/coreos-wordmark-135x40px.png" - var requiredTmpls = []string{ tmplApproval, tmplLogin, @@ -27,65 +27,122 @@ var requiredTmpls = []string{ tmplOOB, } -// TemplateConfig describes. -type TemplateConfig struct { - // TODO(ericchiang): Asking for a directory with a set of templates doesn't indicate - // what the templates should look like and doesn't allow consumers of this package to - // provide their own templates in memory. In the future clean this up. - - // Directory of the templates. If empty, these will be loaded from memory. - Dir string `yaml:"dir"` +type templates struct { + loginTmpl *template.Template + approvalTmpl *template.Template + passwordTmpl *template.Template + oobTmpl *template.Template +} - // Defaults to the CoreOS logo and "dex". - LogoURL string `yaml:"logoURL"` - Issuer string `yaml:"issuerName"` +type webConfig struct { + dir string + logoURL string + issuer string + theme string + issuerURL string } -type globalData struct { - LogoURL string - Issuer string +func join(base, path string) string { + b := strings.HasSuffix(base, "/") + p := strings.HasPrefix(path, "/") + switch { + case b && p: + return base + path[1:] + case b || p: + return base + path + default: + return base + "/" + path + } } -func loadTemplates(config TemplateConfig) (*templates, error) { - var tmpls *template.Template - if config.Dir != "" { - files, err := ioutil.ReadDir(config.Dir) - if err != nil { - return nil, fmt.Errorf("read dir: %v", err) +func dirExists(dir string) error { + stat, err := os.Stat(dir) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("directory %q does not exist", dir) } - filenames := []string{} - for _, file := range files { - if file.IsDir() { - continue - } - filenames = append(filenames, filepath.Join(config.Dir, file.Name())) - } - if len(filenames) == 0 { - return nil, fmt.Errorf("no files in template dir %s", config.Dir) - } - if tmpls, err = template.ParseFiles(filenames...); err != nil { - return nil, fmt.Errorf("parse files: %v", err) + return fmt.Errorf("stat directory %q: %v", dir, err) + } + if !stat.IsDir() { + return fmt.Errorf("path %q is a file not a directory", dir) + } + return nil +} + +// loadWebConfig returns static assets, theme assets, and templates used by the frontend by +// reading the directory specified in the webConfig. +// +// The directory layout is expected to be: +// +// ( web directory ) +// |- static +// |- themes +// | |- (theme name) +// |- templates +// +func loadWebConfig(c webConfig) (static, theme http.Handler, templates *templates, err error) { + if c.theme == "" { + c.theme = "coreos" + } + if c.issuer == "" { + c.issuer = "dex" + } + if c.dir == "" { + c.dir = "./web" + } + if c.logoURL == "" { + c.logoURL = join(c.issuerURL, "theme/logo.png") + } + + if err := dirExists(c.dir); err != nil { + return nil, nil, nil, fmt.Errorf("load web dir: %v", err) + } + + staticDir := filepath.Join(c.dir, "static") + templatesDir := filepath.Join(c.dir, "templates") + themeDir := filepath.Join(c.dir, "themes", c.theme) + + for _, dir := range []string{staticDir, templatesDir, themeDir} { + if err := dirExists(dir); err != nil { + return nil, nil, nil, fmt.Errorf("load dir: %v", err) } - } else { - // Load templates from memory. This code is largely copied from the standard library's - // ParseFiles source code. - // See: https://goo.gl/6Wm4mN - for name, data := range defaultTemplates { - var t *template.Template - if tmpls == nil { - tmpls = template.New(name) - } - if name == tmpls.Name() { - t = tmpls - } else { - t = tmpls.New(name) - } - if _, err := t.Parse(data); err != nil { - return nil, fmt.Errorf("parsing %s: %v", name, err) - } + } + + static = http.FileServer(http.Dir(staticDir)) + theme = http.FileServer(http.Dir(themeDir)) + + templates, err = loadTemplates(c, templatesDir) + return +} + +// loadTemplates parses the expected templates from the provided directory. +func loadTemplates(c webConfig, templatesDir string) (*templates, error) { + files, err := ioutil.ReadDir(templatesDir) + if err != nil { + return nil, fmt.Errorf("read dir: %v", err) + } + + filenames := []string{} + for _, file := range files { + if file.IsDir() { + continue } + filenames = append(filenames, filepath.Join(templatesDir, file.Name())) + } + if len(filenames) == 0 { + return nil, fmt.Errorf("no files in template dir %q", templatesDir) } + funcs := map[string]interface{}{ + "issuer": func() string { return c.issuer }, + "logo": func() string { return c.logoURL }, + "url": func(s string) string { return join(c.issuerURL, s) }, + } + + tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...) + if err != nil { + return nil, fmt.Errorf("parse files: %v", err) + } missingTmpls := []string{} for _, tmplName := range requiredTmpls { if tmpls.Lookup(tmplName) == nil { @@ -95,16 +152,7 @@ func loadTemplates(config TemplateConfig) (*templates, error) { if len(missingTmpls) > 0 { return nil, fmt.Errorf("missing template(s): %s", missingTmpls) } - - if config.LogoURL == "" { - config.LogoURL = coreOSLogoURL - } - if config.Issuer == "" { - config.Issuer = "dex" - } - return &templates{ - globalData: config, loginTmpl: tmpls.Lookup(tmplLogin), approvalTmpl: tmpls.Lookup(tmplApproval), passwordTmpl: tmpls.Lookup(tmplPassword), @@ -118,14 +166,6 @@ var scopeDescriptions = map[string]string{ "email": "View your email", } -type templates struct { - globalData TemplateConfig - loginTmpl *template.Template - approvalTmpl *template.Template - passwordTmpl *template.Template - oobTmpl *template.Template -} - type connectorInfo struct { ID string Name string @@ -142,21 +182,19 @@ func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo, aut sort.Sort(byName(connectors)) data := struct { - TemplateConfig Connectors []connectorInfo AuthReqID string - }{t.globalData, connectors, authReqID} + }{connectors, authReqID} renderTemplate(w, t.loginTmpl, data) } func (t *templates) password(w http.ResponseWriter, authReqID, callback, lastUsername string, lastWasInvalid bool) { data := struct { - TemplateConfig AuthReqID string PostURL string Username string Invalid bool - }{t.globalData, authReqID, callback, lastUsername, lastWasInvalid} + }{authReqID, string(callback), lastUsername, lastWasInvalid} renderTemplate(w, t.passwordTmpl, data) } @@ -170,20 +208,18 @@ func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientN } sort.Strings(accesses) data := struct { - TemplateConfig User string Client string AuthReqID string Scopes []string - }{t.globalData, username, clientName, authReqID, accesses} + }{username, clientName, authReqID, accesses} renderTemplate(w, t.approvalTmpl, data) } func (t *templates) oob(w http.ResponseWriter, code string) { data := struct { - TemplateConfig Code string - }{t.globalData, code} + }{code} renderTemplate(w, t.oobTmpl, data) } diff --git a/server/templates_default.go b/server/templates_default.go deleted file mode 100644 index 651c7411..00000000 --- a/server/templates_default.go +++ /dev/null @@ -1,362 +0,0 @@ -// This file was generated by the makefile. Do not edit. - -package server - -// defaultTemplates is a key for file name to file data of the files in web/templates. -var defaultTemplates = map[string]string{ - "approval.html": `{{ template "header.html" . }} - -<div class="panel"> - <h2 class="heading">Grant Access</h2> - - <hr> - <div class="list-with-title"> - <div class="subtle-text">{{ .Client }} would like to:</div> - {{ range $scope := .Scopes }} - <li class="bullet-point"> - <div class="subtle-text"> - {{ $scope }} - </div> - </li> - {{ end }} - </div> - <hr> - - <div> - <div class="form-row"> - <form method="post"> - <input type="hidden" name="req" value="{{ .AuthReqID }}"/> - <input type="hidden" name="approval" value="approve"> - <button type="submit" class="btn btn-success"> - <span class="btn-text">Grant Access</span> - </button> - </form> - </div> - <div class="form-row"> - <form method="post"> - <input type="hidden" name="req" value="{{ .AuthReqID }}"/> - <input type="hidden" name="approval" value="rejected"> - <button type="submit" class="btn btn-provider"> - <span class="btn-text">Cancel</span> - </button> - </form> - </div> - </div> - -</div> - -{{ template "footer.html" . }} -`, - "footer.html": ` </div> - </body> -</html> -`, - "header.html": `<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> - <title>{{ .Issuer }}</title> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <style> - * { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - } - - html, - body { - margin: 0; - background-color: #efefef; - font-family: 'Source Sans Pro', Helvetica, sans-serif; - color: #333; - } - a { - color: #428BCA; - text-decoration: none; - } - a:active, a:hover, a:visited { - color: #2A6596; - text-decoration: underline; - } - #navbar { - background-color: #fff; - color: #333; - height: 46px; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - font-size: 13px; - font-weight: 100; - overflow: hidden; - padding: 0 10px; - } - #navbar-logo-wrap { - width: 300px; - height: 100%; - display: inline-block; - overflow: hidden; - padding: 10px 15px; - } - #navbar-logo { - height: 100%; - max-height: 25px; - } - #container { - margin: 45px auto; - text-align: center; - max-width: 500px; - min-width: 320px; - } - .heading { - font-size: 20px; - font-weight: 500; - margin-top: 0; - margin-bottom: 10px; - } - .footer { - margin: 30px; - } - .input-label-right { - position: absolute; - right: 0; - bottom: 0; - } - .input-desc { - width: 250px; - margin: 4px auto; - text-align: left; - position: relative; - } - .subtle-text { - color: #999; - font-size: 12px; - } - .panel { - background-color: #fff; - padding: 30px; - box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.5); - } - .explain { - font-size: 13px; - color: #666; - } - - .btn { - box-shadow: inset 0 1px 0px rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.25), 0 0px 1px rgba(0, 0, 0, 0.25); - padding: 0; - font-size: 14px; - border-radius: 4px; - border: none; - cursor: pointer; - font-size: 16px; - } - .btn:focus { - outline: none; - } - .btn:active { - outline: none; - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - } - .btn-primary { - color: #fff; - background-color: #333; - padding: 6px 12px; - min-width: 200px; - border: none; - } - .btn-primary:hover { - background-color: #666; - color: #fff; - } - .btn-provider { - background-color: #fff; - color: #333; - width: 250px; - } - .btn-provider:hover { - color: #999; - } - .btn-success { - background-color: #2FC98E; - color: #fff; - width: 250px; - } - .btn-success:hover { - background-color: #49E3A8; - } - .btn-icon { - width: 36px; - height: 36px; - float: left; - margin-right: 5px; - background-repeat: no-repeat; - background-position: center; - background-size: 24px; - } - .btn-icon-google { - background-color: #DB4437; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjM2cHgiIGhlaWdodD0iMzdweCIgdmlld0JveD0iMCAwIDM2IDM3IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5TaGFwZSArIGcrPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSIzLjg0OTMxNTA3JSIgeTE9IjM0LjQ3MzI2MiUiIHgyPSI5Mi4yODU0Nzk1JSIgeTI9IjcwLjIyMzI2MiUiIGlkPSJsaW5lYXJHcmFkaWVudC0xIj4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzNFMjcyMyIgc3RvcC1vcGFjaXR5PSIwLjIiIG9mZnNldD0iMCUiPjwvc3RvcD4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzNFMjcyMyIgc3RvcC1vcGFjaXR5PSIwLjAyIiBvZmZzZXQ9IjEwMCUiPjwvc3RvcD4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9IkEuMS1WZXJpZnktRW1haWwtU2NyZWVuX2xvZ2luLSIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQwNy4wMDAwMDAsIC0yNzIuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJTaGFwZS0rLWcrIiBza2V0Y2g6dHlwZT0iTVNMYXllckdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0MDcuMDAwMDAwLCAyNzIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMzUuOTYzOTg4MSwxNS4zMjM2OTA1IEwxOC43OTIzMjE0LDAuNzUyMDIzODEgTDEwLjc1Nzk3NjIsMC43NTIwMjM4MSBDNS4xNDQ5NDA0OCwwLjc1MjAyMzgxIDIuMzkzNDUyMzgsNC4xNjM4NjkwNSAyLjM5MzQ1MjM4LDguMDE1OTUyMzggQzIuMzkzNDUyMzgsMTAuOTg3NTU5NSA0LjgxNDc2MTksMTQuMjg5MzQ1MiA4Ljg4Njk2NDI5LDE0LjI4OTM0NTIgTDkuODc3NSwxNC4yODkzNDUyIEM5LjY1NzM4MDk1LDE0LjYxOTUyMzggOS41NDczMjE0MywxNS4yNzk4ODEgOS41NDczMjE0MywxNS43MjAxMTkgQzkuNTQ3MzIxNDMsMTYuODIwNzE0MyA5Ljk4NzU1OTUyLDE3LjM3MTAxMTkgMTAuNjQ3OTE2NywxOC4wMzEzNjkgQzguODg2OTY0MjksMTguMTQxNDI4NiA1LjU4NTE3ODU3LDE4LjQ3MTYwNzEgMy4xNjM4NjkwNSwyMC4wMTI0NDA1IEMwLjg1MjYxOTA0OCwyMS4zMzMxNTQ4IDAuMTkyMjYxOTA1LDIzLjMxNDIyNjIgMC4xOTIyNjE5MDUsMjQuNzQ1IEMwLjE5MjI2MTkwNSwyNS45NTU2NTQ4IDAuNjMyNSwyNy4wNTYyNSAxLjYyMzAzNTcxLDI4LjA0Njc4NTcgTDE1LjQ5MDUzNTcsMzYuOTE0Mjg1NyBMMzUuOTYzOTg4MSwzNi45MTQyODU3IEwzNS45NjM5ODgxLDE1LjMyMzY5MDUgWiIgaWQ9IlNoYXBlIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50LTEpIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8ZyBpZD0iZysiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMTkyMjYyLCAxLjE5MjI2MikiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xOC42MDAwNTk1LC0wLjExMDA1OTUyNCBMMTguNjAwMDU5NSwtMC40NDAyMzgwOTUgTDEwLjU2NTcxNDMsLTAuMTEwMDU5NTI0IEM0Ljk1MjY3ODU3LC0wLjExMDA1OTUyNCAyLjIwMTE5MDQ4LDMuMzAxNzg1NzEgMi4yMDExOTA0OCw3LjE1Mzg2OTA1IEMyLjIwMTE5MDQ4LDEwLjEyNTQ3NjIgNC42MjI1LDEzLjQyNzI2MTkgOC42OTQ3MDIzOCwxMy40MjcyNjE5IEw5LjY4NTIzODEsMTMuNDI3MjYxOSBDOS40NjUxMTkwNSwxMy43NTc0NDA1IDkuMzU1MDU5NTIsMTQuNDE3Nzk3NiA5LjM1NTA1OTUyLDE0Ljk2ODA5NTIgQzkuMzU1MDU5NTIsMTYuMDY4NjkwNSA5Ljc5NTI5NzYyLDE2LjYxODk4ODEgMTAuNDU1NjU0OCwxNy4yNzkzNDUyIEM4LjY5NDcwMjM4LDE3LjM4OTQwNDggNS4zOTI5MTY2NywxNy43MTk1ODMzIDIuOTcxNjA3MTQsMTkuMjYwNDE2NyBDMC42NjAzNTcxNDMsMjAuNTgxMTMxIDAsMjIuNTYyMjAyNCAwLDIzLjk5Mjk3NjIgQzAsMjYuODU0NTIzOCAyLjc1MTQ4ODEsMjkuNDk1OTUyNCA4LjM2NDUyMzgxLDI5LjQ5NTk1MjQgQzE1LjA3ODE1NDgsMjkuNDk1OTUyNCAxOC42MDAwNTk1LDI1Ljg2Mzk4ODEgMTguNjAwMDU5NSwyMi4yMzIwMjM4IEMxOC42MDAwNTk1LDE5LjQ4MDUzNTcgMTcuMDU5MjI2MiwxOC4xNTk4MjE0IDE1LjI5ODI3MzgsMTYuNzI5MDQ3NiBMMTMuODY3NSwxNS42Mjg0NTI0IEMxMy40MjcyNjE5LDE1LjI5ODI3MzggMTIuOTg3MDIzOCwxNC44NTgwMzU3IDEyLjk4NzAyMzgsMTMuOTc3NTU5NSBDMTIuOTg3MDIzOCwxMy4wOTcwODMzIDEzLjUzNzMyMTQsMTIuNDM2NzI2MiAxNC4wODc2MTksMTIuMTA2NTQ3NiBDMTUuNzM4NTExOSwxMC43ODU4MzMzIDE3LjM4OTQwNDgsOS40NjUxMTkwNSAxNy4zODk0MDQ4LDYuNDkzNTExOSBDMTcuMzg5NDA0OCwzLjc0MjAyMzgxIDE1LjczODUxMTksMi4zMTEyNSAxNC43NDc5NzYyLDEuNTQwODMzMzMgTDE2Ljk0OTE2NjcsMS41NDA4MzMzMyBMMTguNjAwMDU5NSwtMC4xMTAwNTk1MjQgTDE4LjYwMDA1OTUsLTAuMTEwMDU5NTI0IFogTTE2LjA2ODY5MDUsMjMuNjYyNzk3NiBDMTYuMDY4NjkwNSwyNS44NjM5ODgxIDE0LjA4NzYxOSwyNy44NDUwNTk1IDEwLjM0NTU5NTIsMjcuODQ1MDU5NSBDNi4xNjMzMzMzMywyNy44NDUwNTk1IDMuNTIxOTA0NzYsMjUuNzUzOTI4NiAzLjUyMTkwNDc2LDIzLjExMjUgQzMuNTIxOTA0NzYsMjAuMzYxMDExOSA2LjA1MzI3MzgxLDE5LjM3MDQ3NjIgNi44MjM2OTA0OCwxOS4wNDAyOTc2IEM4LjQ3NDU4MzMzLDE4LjQ5IDEwLjU2NTcxNDMsMTguMzc5OTQwNSAxMC44OTU4OTI5LDE4LjM3OTk0MDUgTDExLjc3NjM2OSwxOC4zNzk5NDA1IEMxNC43NDc5NzYyLDIwLjU4MTEzMSAxNi4wNjg2OTA1LDIxLjY4MTcyNjIgMTYuMDY4NjkwNSwyMy42NjI3OTc2IEwxNi4wNjg2OTA1LDIzLjY2Mjc5NzYgWiBNMTAuNTY1NzE0MywxMi4xMDY1NDc2IEM3LjI2MzkyODU3LDEyLjEwNjU0NzYgNS41MDI5NzYxOSw4LjI1NDQ2NDI5IDUuNTAyOTc2MTksNS4yODI4NTcxNCBDNS41MDI5NzYxOSwyLjUzMTM2OTA1IDcuMjYzOTI4NTcsMS4yMTA2NTQ3NiA5LjEzNDk0MDQ4LDEuMjEwNjU0NzYgQzEyLjY1Njg0NTIsMS4yMTA2NTQ3NiAxNC40MTc3OTc2LDUuNjEzMDM1NzEgMTQuNDE3Nzk3Niw4LjI1NDQ2NDI5IEMxNC4zMDc3MzgxLDExLjQ0NjE5MDUgMTEuNTU2MjUsMTIuMTA2NTQ3NiAxMC41NjU3MTQzLDEyLjEwNjU0NzYgTDEwLjU2NTcxNDMsMTIuMTA2NTQ3NiBaIE0yNi40MTQyODU3LDEyLjk4NzAyMzggTDI2LjQxNDI4NTcsOC4wMzQzNDUyNCBMMjQuNzYzMzkyOSw4LjAzNDM0NTI0IEwyNC43NjMzOTI5LDEyLjk4NzAyMzggTDE5LjgxMDcxNDMsMTIuOTg3MDIzOCBMMTkuODEwNzE0MywxNC42Mzc5MTY3IEwyNC43NjMzOTI5LDE0LjYzNzkxNjcgTDI0Ljc2MzM5MjksMTkuNTkwNTk1MiBMMjYuNDE0Mjg1NywxOS41OTA1OTUyIEwyNi40MTQyODU3LDE0LjYzNzkxNjcgTDMxLjM2Njk2NDMsMTQuNjM3OTE2NyBMMzEuMzY2OTY0MywxMi45ODcwMjM4IEwyNi40MTQyODU3LDEyLjk4NzAyMzggTDI2LjQxNDI4NTcsMTIuOTg3MDIzOCBaIiBpZD0iU2hhcGUiIG9wYWNpdHk9IjAuMTYiIGZpbGw9IiMzRTI3MjMiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTguNjAwMDU5NSwtMC40NDAyMzgwOTUgTDEwLjU2NTcxNDMsLTAuNDQwMjM4MDk1IEM0Ljk1MjY3ODU3LC0wLjQ0MDIzODA5NSAyLjIwMTE5MDQ4LDIuOTcxNjA3MTQgMi4yMDExOTA0OCw2LjgyMzY5MDQ4IEMyLjIwMTE5MDQ4LDkuNzk1Mjk3NjIgNC42MjI1LDEzLjA5NzA4MzMgOC42OTQ3MDIzOCwxMy4wOTcwODMzIEw5LjY4NTIzODEsMTMuMDk3MDgzMyBDOS40NjUxMTkwNSwxMy40MjcyNjE5IDkuMzU1MDU5NTIsMTQuMDg3NjE5IDkuMzU1MDU5NTIsMTQuNTI3ODU3MSBDOS4zNTUwNTk1MiwxNS42Mjg0NTI0IDkuNzk1Mjk3NjIsMTYuMTc4NzUgMTAuNDU1NjU0OCwxNi44MzkxMDcxIEM4LjY5NDcwMjM4LDE2Ljk0OTE2NjcgNS4zOTI5MTY2NywxNy4yNzkzNDUyIDIuOTcxNjA3MTQsMTguODIwMTc4NiBDMC42NjAzNTcxNDMsMjAuMTQwODkyOSAwLDIyLjEyMTk2NDMgMCwyMy41NTI3MzgxIEMwLDI2LjQxNDI4NTcgMi43NTE0ODgxLDI5LjA1NTcxNDMgOC4zNjQ1MjM4MSwyOS4wNTU3MTQzIEMxNS4wNzgxNTQ4LDI5LjA1NTcxNDMgMTguNjAwMDU5NSwyNS40MjM3NSAxOC42MDAwNTk1LDIxLjc5MTc4NTcgQzE4LjYwMDA1OTUsMTkuMDQwMjk3NiAxNy4wNTkyMjYyLDE3LjcxOTU4MzMgMTUuMjk4MjczOCwxNi4yODg4MDk1IEwxMy44Njc1LDE1LjE4ODIxNDMgQzEzLjQyNzI2MTksMTQuODU4MDM1NyAxMi45ODcwMjM4LDE0LjQxNzc5NzYgMTIuOTg3MDIzOCwxMy41MzczMjE0IEMxMi45ODcwMjM4LDEyLjY1Njg0NTIgMTMuNTM3MzIxNCwxMS45OTY0ODgxIDE0LjA4NzYxOSwxMS42NjYzMDk1IEMxNS43Mzg1MTE5LDEwLjM0NTU5NTIgMTcuMzg5NDA0OCw5LjAyNDg4MDk1IDE3LjM4OTQwNDgsNi4wNTMyNzM4MSBDMTcuMzg5NDA0OCwzLjMwMTc4NTcxIDE1LjczODUxMTksMS44NzEwMTE5IDE0Ljc0Nzk3NjIsMS4xMDA1OTUyNCBMMTYuOTQ5MTY2NywxLjEwMDU5NTI0IEwxOC42MDAwNTk1LC0wLjQ0MDIzODA5NSBMMTguNjAwMDU5NSwtMC40NDAyMzgwOTUgWiBNMTYuMDY4NjkwNSwyMy4zMzI2MTkgQzE2LjA2ODY5MDUsMjUuNTMzODA5NSAxNC4wODc2MTksMjcuNTE0ODgxIDEwLjM0NTU5NTIsMjcuNTE0ODgxIEM2LjE2MzMzMzMzLDI3LjUxNDg4MSAzLjUyMTkwNDc2LDI1LjQyMzc1IDMuNTIxOTA0NzYsMjIuNzgyMzIxNCBDMy41MjE5MDQ3NiwyMC4wMzA4MzMzIDYuMDUzMjczODEsMTkuMDQwMjk3NiA2LjgyMzY5MDQ4LDE4LjcxMDExOSBDOC40NzQ1ODMzMywxOC4xNTk4MjE0IDEwLjU2NTcxNDMsMTguMDQ5NzYxOSAxMC44OTU4OTI5LDE4LjA0OTc2MTkgTDExLjc3NjM2OSwxOC4wNDk3NjE5IEMxNC43NDc5NzYyLDIwLjI1MDk1MjQgMTYuMDY4NjkwNSwyMS4zNTE1NDc2IDE2LjA2ODY5MDUsMjMuMzMyNjE5IEwxNi4wNjg2OTA1LDIzLjMzMjYxOSBaIE0xMC41NjU3MTQzLDExLjg4NjQyODYgQzcuMjYzOTI4NTcsMTEuODg2NDI4NiA1LjUwMjk3NjE5LDguMDM0MzQ1MjQgNS41MDI5NzYxOSw1LjA2MjczODEgQzUuNTAyOTc2MTksMi4zMTEyNSA3LjI2MzkyODU3LDAuOTkwNTM1NzE0IDkuMTM0OTQwNDgsMC45OTA1MzU3MTQgQzEyLjY1Njg0NTIsMC45OTA1MzU3MTQgMTQuNDE3Nzk3Niw1LjM5MjkxNjY3IDE0LjQxNzc5NzYsOC4wMzQzNDUyNCBDMTQuMzA3NzM4MSwxMS4yMjYwNzE0IDExLjU1NjI1LDExLjg4NjQyODYgMTAuNTY1NzE0MywxMS44ODY0Mjg2IEwxMC41NjU3MTQzLDExLjg4NjQyODYgWiBNMjYuNDE0Mjg1NywxMi42NTY4NDUyIEwyNi40MTQyODU3LDcuNzA0MTY2NjcgTDI0Ljc2MzM5MjksNy43MDQxNjY2NyBMMjQuNzYzMzkyOSwxMi42NTY4NDUyIEwxOS44MTA3MTQzLDEyLjY1Njg0NTIgTDE5LjgxMDcxNDMsMTQuMzA3NzM4MSBMMjQuNzYzMzkyOSwxNC4zMDc3MzgxIEwyNC43NjMzOTI5LDE5LjI2MDQxNjcgTDI2LjQxNDI4NTcsMTkuMjYwNDE2NyBMMjYuNDE0Mjg1NywxNC4zMDc3MzgxIEwzMS4zNjY5NjQzLDE0LjMwNzczODEgTDMxLjM2Njk2NDMsMTIuNjU2ODQ1MiBMMjYuNDE0Mjg1NywxMi42NTY4NDUyIEwyNi40MTQyODU3LDEyLjY1Njg0NTIgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjRjFGMUYxIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTkuNzk1Mjk3NjIsMTMuMzE3MjAyNCBMOS43OTUyOTc2MiwxMy4wOTcwODMzIEM5LjU3NTE3ODU3LDEzLjQyNzI2MTkgOS40NjUxMTkwNSwxNC4wODc2MTkgOS40NjUxMTkwNSwxNC41Mjc4NTcxIEw5LjQ2NTExOTA1LDE0LjYzNzkxNjcgQzkuNDY1MTE5MDUsMTQuMTk3Njc4NiA5LjU3NTE3ODU3LDEzLjY0NzM4MSA5Ljc5NTI5NzYyLDEzLjMxNzIwMjQgTDkuNzk1Mjk3NjIsMTMuMzE3MjAyNCBaIE0xMC40NTU2NTQ4LDE2Ljk0OTE2NjcgQzguNjk0NzAyMzgsMTcuMDU5MjI2MiA1LjM5MjkxNjY3LDE3LjM4OTQwNDggMi45NzE2MDcxNCwxOC45MzAyMzgxIEMwLjY2MDM1NzE0MywyMC4yNTA5NTI0IDAsMjIuMjMyMDIzOCAwLDIzLjY2Mjc5NzYgTDAsMjMuNzcyODU3MSBDMC4xMTAwNTk1MjQsMjIuMzQyMDgzMyAwLjc3MDQxNjY2NywyMC40NzEwNzE0IDIuOTcxNjA3MTQsMTkuMTUwMzU3MSBDNS4zOTI5MTY2NywxNy43MTk1ODMzIDguNjk0NzAyMzgsMTcuMjc5MzQ1MiAxMC40NTU2NTQ4LDE3LjE2OTI4NTcgTDEwLjQ1NTY1NDgsMTYuOTQ5MTY2NyBMMTAuNDU1NjU0OCwxNi45NDkxNjY3IFogTTEwLjM0NTU5NTIsMjcuNTE0ODgxIEM2LjI3MzM5Mjg2LDI3LjUxNDg4MSAzLjYzMTk2NDI5LDI1LjUzMzgwOTUgMy41MjE5MDQ3NiwyMi44OTIzODEgTDMuNTIxOTA0NzYsMjMuMDAyNDQwNSBDMy41MjE5MDQ3NiwyNS42NDM4NjkgNi4xNjMzMzMzMywyNy43MzUgMTAuMzQ1NTk1MiwyNy43MzUgQzE0LjA4NzYxOSwyNy43MzUgMTYuMDY4NjkwNSwyNS43NTM5Mjg2IDE2LjA2ODY5MDUsMjMuNTUyNzM4MSBMMTYuMDY4NjkwNSwyMy40NDI2Nzg2IEMxNS45NTg2MzEsMjUuNjQzODY5IDEzLjk3NzU1OTUsMjcuNTE0ODgxIDEwLjM0NTU5NTIsMjcuNTE0ODgxIEwxMC4zNDU1OTUyLDI3LjUxNDg4MSBaIE0xNC4zMDc3MzgxLDguMjU0NDY0MjkgTDE0LjMwNzczODEsOC4xNDQ0MDQ3NiBDMTQuMTk3Njc4NiwxMS4zMzYxMzEgMTEuNTU2MjUsMTEuODg2NDI4NiAxMC40NTU2NTQ4LDExLjg4NjQyODYgQzcuMjYzOTI4NTcsMTEuODg2NDI4NiA1LjM5MjkxNjY3LDguMTQ0NDA0NzYgNS4zOTI5MTY2Nyw1LjE3Mjc5NzYyIEw1LjM5MjkxNjY3LDUuMjgyODU3MTQgQzUuMzkyOTE2NjcsOC4yNTQ0NjQyOSA3LjE1Mzg2OTA1LDEyLjEwNjU0NzYgMTAuNDU1NjU0OCwxMi4xMDY1NDc2IEMxMS41NTYyNSwxMi4xMDY1NDc2IDE0LjMwNzczODEsMTEuNDQ2MTkwNSAxNC4zMDc3MzgxLDguMjU0NDY0MjkgTDE0LjMwNzczODEsOC4yNTQ0NjQyOSBaIE0xNS40MDgzMzMzLDE2LjI4ODgwOTUgTDEzLjk3NzU1OTUsMTUuMTg4MjE0MyBDMTMuNTM3MzIxNCwxNC44NTgwMzU3IDEzLjIwNzE0MjksMTQuNDE3Nzk3NiAxMy4wOTcwODMzLDEzLjY0NzM4MSBMMTMuMDk3MDgzMywxMy43NTc0NDA1IEMxMy4wOTcwODMzLDE0LjYzNzkxNjcgMTMuNTM3MzIxNCwxNS4wNzgxNTQ4IDEzLjk3NzU1OTUsMTUuNDA4MzMzMyBMMTUuNDA4MzMzMywxNi41MDg5Mjg2IEMxNy4wNTkyMjYyLDE3LjkzOTcwMjQgMTguNjAwMDU5NSwxOS4yNjA0MTY3IDE4LjcxMDExOSwyMS43OTE3ODU3IEwxOC43MTAxMTksMjEuNjgxNzI2MiBDMTguNjAwMDU5NSwxOS4wNDAyOTc2IDE3LjA1OTIyNjIsMTcuODI5NjQyOSAxNS40MDgzMzMzLDE2LjI4ODgwOTUgTDE1LjQwODMzMzMsMTYuMjg4ODA5NSBaIE0yNi40MTQyODU3LDEyLjY1Njg0NTIgTDI2LjQxNDI4NTcsMTIuODc2OTY0MyBMMzEuMzY2OTY0MywxMi44NzY5NjQzIEwzMS4zNjY5NjQzLDEyLjY1Njg0NTIgTDI2LjQxNDI4NTcsMTIuNjU2ODQ1MiBMMjYuNDE0Mjg1NywxMi42NTY4NDUyIFogTTE3LjQ5OTQ2NDMsNi4yNzMzOTI4NiBMMTcuNDk5NDY0Myw2LjE2MzMzMzMzIEMxNy40OTk0NjQzLDMuNDExODQ1MjQgMTUuODQ4NTcxNCwxLjk4MTA3MTQzIDE0Ljg1ODAzNTcsMS4yMTA2NTQ3NiBMMTQuODU4MDM1NywxLjU0MDgzMzMzIEMxNS44NDg1NzE0LDIuMjAxMTkwNDggMTcuNDk5NDY0MywzLjYzMTk2NDI5IDE3LjQ5OTQ2NDMsNi4yNzMzOTI4NiBMMTcuNDk5NDY0Myw2LjI3MzM5Mjg2IFogTTI2LjQxNDI4NTcsNy43MDQxNjY2NyBMMjQuNzYzMzkyOSw3LjcwNDE2NjY3IEwyNC43NjMzOTI5LDcuOTI0Mjg1NzEgTDI2LjQxNDI4NTcsNy45MjQyODU3MSBMMjYuNDE0Mjg1Nyw3LjcwNDE2NjY3IEwyNi40MTQyODU3LDcuNzA0MTY2NjcgWiBNMi4zMTEyNSw2LjgyMzY5MDQ4IEwyLjMxMTI1LDYuOTMzNzUgQzIuNDIxMzA5NTIsMy4wODE2NjY2NyA1LjA2MjczODEsLTAuMTEwMDU5NTI0IDEwLjU2NTcxNDMsLTAuMTEwMDU5NTI0IEwxOC4zNzk5NDA1LC0wLjExMDA1OTUyNCBMMTguNzEwMTE5LC0wLjQ0MDIzODA5NSBMMTAuNjc1NzczOCwtMC40NDAyMzgwOTUgQzQuOTUyNjc4NTcsLTAuNDQwMjM4MDk1IDIuMzExMjUsMi45NzE2MDcxNCAyLjMxMTI1LDYuODIzNjkwNDggTDIuMzExMjUsNi44MjM2OTA0OCBaIE0xOS44MTA3MTQzLDEyLjk4NzAyMzggTDI0Ljc2MzM5MjksMTIuOTg3MDIzOCBMMjQuNzYzMzkyOSwxMi43NjY5MDQ4IEwxOS44MTA3MTQzLDEyLjc2NjkwNDggTDE5LjgxMDcxNDMsMTIuOTg3MDIzOCBMMTkuODEwNzE0MywxMi45ODcwMjM4IFoiIGlkPSJTaGFwZSIgZmlsbC1vcGFjaXR5PSIwLjY0IiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==); - } - .btn-icon-local { - background-color: #84B6EF; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAwIDI0IDIwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5SZWN0YW5nbGUgMjkxICsgUGF0aCAyMzI8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8ZyBpZD0iQS4xLVZlcmlmeS1FbWFpbC1TY3JlZW5fbG9naW4tIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDA5LjAwMDAwMCwgLTIwOS4wMDAwMDApIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iI0ZGRkZGRiI+CiAgICAgICAgICAgIDxnIGlkPSJSZWN0YW5nbGUtMzktQ29weS02LSstRW1haWwtQ29weS0rLVJlY3RhbmdsZS0yOTAtKy1SZWN0YW5nbGUtMjkxLSstUGF0aC0yMzIiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQwMC4wMDAwMDAsIDE5Ny4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJSZWN0YW5nbGUtMjkwLSstUmVjdGFuZ2xlLTI5MS0rLVBhdGgtMjMyIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj4KICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iUmVjdGFuZ2xlLTI5MS0rLVBhdGgtMjMyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5LjAwMDAwMCwgMTIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS0yOTEiIHg9IjAiIHk9IjAiIHdpZHRoPSIyNCIgaGVpZ2h0PSIxOS4zNSI+PC9yZWN0PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsMS45MjcyNzEzOSBMMTEuNjExMzAxOSwxMi45IEwyNCwxLjE5MjYyODgxIiBpZD0iUGF0aC0yMzIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+); - } - .btn-icon-coreos { - /* B&W CoreOS SVG logo */ - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSIjNjY2IiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDIxNSAyMTUiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KICA8Zz4NCiAgICA8Zz4NCiAgICAgIDxwYXRoIGQ9Ik0xMDcuNDc5LDEuMDc1Yy01OC42NzcsMC0xMDYuNDA0LDQ3LjczLTEwNi40MDQsMTA2LjM5OGMwLDU4LjY3Miw0Ny43MjcsMTA2LjM5OSwxMDYuNDA0LDEwNi4zOTkNCiAgICAgICAgICAgICAgYzU4LjY1OSwwLDEwNi4zOS00Ny43MjcsMTA2LjM5LTEwNi4zOTlDMjEzLjg2OSw0OC44MDUsMTY2LjEzOCwxLjA3NSwxMDcuNDc5LDEuMDc1eiBNMTQ3LjQ0OSwxMzQuNjI3DQogICAgICAgICAgICAgIGMtMC44OCwwLjEyOC0xLjc0OSwwLjI1MS0yLjYzMiwwLjM2NGMtOC4wMywxLjAzOC0xNi42MDIsMS43NDMtMjUuNTYxLDIuMDc4Yy0zLjg1NiwwLjE0NC03Ljc5MywwLjIzMS0xMS43NzYsMC4yMzENCiAgICAgICAgICAgICAgYy0zLjk5NSwwLTcuOTItMC4wODYtMTEuNzg4LTAuMjMxYy04Ljk0OC0wLjMzNS0xNy41MjYtMS4wNC0yNS41NDktMi4wNzhjLTAuNzE2LTUuOTg3LTEuMjAxLTEyLjIxNi0xLjQ0My0xOC42MjMNCiAgICAgICAgICAgICAgYy0wLjExNi0yLjkyNi0wLjE3My01Ljg5Ni0wLjE3My04Ljg5NWMwLTMuMDAyLDAuMDU3LTUuOTY2LDAuMTczLTguODk4YzAuMjQzLTYuNDA4LDAuNzI4LTEyLjYzMywxLjQ0My0xOC42Mg0KICAgICAgICAgICAgICBjMC4xNDQtMS4yNDYsMC4zMDYtMi40ODUsMC40NzMtMy43MDljNS4yNDEtMzguMDQsMTkuNzUyLTY1LjQwOCwzNi44NjMtNjUuNDA4YzUzLjM2NCwwLDk2LjYzMiw0My4yNjIsOTYuNjMzLDk2LjYzNQ0KICAgICAgICAgICAgICBDMjA0LjExMiwxMTkuNTQ3LDE4MC44NjYsMTI5LjkzNCwxNDcuNDQ5LDEzNC42Mjd6Ii8+DQogICAgICA8cGF0aCBkPSJNMTQ3LjQ0OCw4MC4zMTZjLTAuOTY5LTEuNDE0LTIuMDA5LTIuNzY4LTMuMTE3LTQuMDY5Yy04Ljg2Ni0xMC40NTEtMjIuMDc0LTE3LjA5Mi0zNi44NTItMTcuMDkyDQogICAgICAgICAgICAgIGMtNC43OTEsMC05LjA1Nyw3LjMzMy0xMS43ODgsMTguNzJjLTEuMDg1LDQuNTQtMS45MjgsOS43MjEtMi40NywxNS4zNDNjLTAuNDI4LDQuNTA1LTAuNjU4LDkuMjk3LTAuNjU4LDE0LjI1NQ0KICAgICAgICAgICAgICBzMC4yMzEsOS43NTEsMC42NTgsMTQuMjUyYzQuNTA4LDAuNDI4LDkuMjkzLDAuNjU3LDE0LjI1OCwwLjY1N2M0Ljk1OSwwLDkuNzQ0LTAuMjMsMTQuMjUyLTAuNjU3DQogICAgICAgICAgICAgIGM5LjkxMS0wLjk0LDE4LjQ2Ni0yLjg0NiwyNC41MjctNS4zNTdjNS45ODYtMi40NzYsOS41MjgtNS41NTksOS41MjgtOC44OTVDMTU1Ljc4Niw5Ny40MDcsMTUyLjcxMiw4OC4wNTcsMTQ3LjQ0OCw4MC4zMTZ6Ii8+DQogICAgPC9nPg0KICA8L2c+DQo8L3N2Zz4NCg==); - } - .btn-icon-github { - background-color: #F5F5F5; - background-image: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTggMEMzLjU4IDAgMCAzLjU4IDAgOGMwIDMuNTQgMi4yOSA2LjUzIDUuNDcgNy41OSAwLjQgMC4wNyAwLjU1LTAuMTcgMC41NS0wLjM4IDAtMC4xOS0wLjAxLTAuODItMC4wMS0xLjQ5LTIuMDEgMC4zNy0yLjUzLTAuNDktMi42OS0wLjk0LTAuMDktMC4yMy0wLjQ4LTAuOTQtMC44Mi0xLjEzLTAuMjgtMC4xNS0wLjY4LTAuNTItMC4wMS0wLjUzIDAuNjMtMC4wMSAxLjA4IDAuNTggMS4yMyAwLjgyIDAuNzIgMS4yMSAxLjg3IDAuODcgMi4zMyAwLjY2IDAuMDctMC41MiAwLjI4LTAuODcgMC41MS0xLjA3LTEuNzgtMC4yLTMuNjQtMC44OS0zLjY0LTMuOTUgMC0wLjg3IDAuMzEtMS41OSAwLjgyLTIuMTUtMC4wOC0wLjItMC4zNi0xLjAyIDAuMDgtMi4xMiAwIDAgMC42Ny0wLjIxIDIuMiAwLjgyIDAuNjQtMC4xOCAxLjMyLTAuMjcgMi0wLjI3IDAuNjggMCAxLjM2IDAuMDkgMiAwLjI3IDEuNTMtMS4wNCAyLjItMC44MiAyLjItMC44MiAwLjQ0IDEuMSAwLjE2IDEuOTIgMC4wOCAyLjEyIDAuNTEgMC41NiAwLjgyIDEuMjcgMC44MiAyLjE1IDAgMy4wNy0xLjg3IDMuNzUtMy42NSAzLjk1IDAuMjkgMC4yNSAwLjU0IDAuNzMgMC41NCAxLjQ4IDAgMS4wNy0wLjAxIDEuOTMtMC4wMSAyLjIgMCAwLjIxIDAuMTUgMC40NiAwLjU1IDAuMzhDMTMuNzEgMTQuNTMgMTYgMTEuNTMgMTYgOCAxNiAzLjU4IDEyLjQyIDAgOCAweiIgLz4KPC9zdmc+Cg==); - } - .btn-icon-bitbucket { - background-color: #205081; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwMCIgd2lkdGg9Ijc4NS43MTQiPjxwYXRoIGQ9Ik00NTQuNzcgNDc5LjM4NnE0LjQ2NCAzNS4xNTQgLTI4LjE3OSA1Ni4zNTh0LTYyLjIxNyAzLjM0OHEtMjEuNzYyIC05LjQ4NiAtMjkuODUzIC0zMi4zNjR0LS4yNzkgLTQ1Ljc1NiAyOS4wMTYgLTMyLjM2NHEyMC4wODggLTEwLjA0NCA0MC40NTUgLTYuNjk2dDM1LjcxMiAxOS44MDkgMTUuMzQ1IDM3LjY2NXptNjEuOTM4IC0xMS43MThxLTcuODEyIC01OS43MDYgLTYzLjA1NCAtOTEuNTEydC0xMDkuOTI2IC03LjI1NHEtMzUuMTU0IDE1LjYyNCAtNTYuMDc5IDQ5LjM4M3QtMTkuMjUxIDcyLjI2MXEyLjIzMiA1MC43NzggNDMuMjQ1IDg2LjQ5dDkyLjM0OSAzMS4yNDhxNTAuNzc4IC00LjQ2NCA4NC44MTYgLTQ2Ljg3MnQyNy45IC05My43NDR6bTEzMy4zNjIgLTMwMi40MzZxLTExLjE2IC0xNS4wNjYgLTMxLjI0OCAtMjQuODMxdC0zMi4zNjQgLTEyLjI3NiAtMzkuNjE4IC02Ljk3NXEtMTYyLjM3OCAtMjYuMjI2IC0zMTUuODI4IDEuMTE2IC0yMy45OTQgMy45MDYgLTM2LjgyOCA2LjY5NnQtMzAuNjkgMTIuMjc2IC0yNy45IDIzLjk5NHExNi43NCAxNS42MjQgNDIuNDA4IDI1LjM4OXQ0MS4wMTMgMTIuMjc2IDQ4LjgyNSA2LjQxN3ExMjcuMjI0IDE2LjE4MiAyNDkuOTg0IC41NTggMzUuMTU0IC00LjQ2NCA0OS45NDEgLTYuNjk2dDQwLjQ1NSAtMTEuOTk3IDQxLjg1IC0yNS45NDd6bTMxLjgwNiA1NzcuNTNxLTQuNDY0IDE0LjUwOCAtOC42NDkgNDIuNjg3dC03LjgxMiA0Ni44NzIgLTE1LjkwMyAzOS4wNiAtMzIuMzY0IDMxLjUyN3EtNDcuOTg4IDI2Ljc4NCAtMTA1Ljc0MSAzOS44OTd0LTExMi43MTYgMTIuMjc2IC0xMTIuNDM3IC0xMC4zMjNxLTI1LjY2OCAtNC40NjQgLTQ1LjQ3NyAtMTAuMDQ0dC00Mi42ODcgLTE1LjA2NiAtNDAuNzM0IC0yNC4yNzMgLTI5LjAxNiAtMzQuMzE3cS0xMy45NSAtNTMuNTY4IC0zMS44MDYgLTE2Mi45MzZsMy4zNDggLTguOTI4IDEwLjA0NCAtNS4wMjJxMTI0LjQzNCA4Mi41ODQgMjgyLjYyNyA4Mi41ODR0MjgzLjE4NSAtODIuNTg0cTExLjcxOCAzLjM0OCAxMy4zOTIgMTIuODM0dC0yLjc5IDI1LjExIC00LjQ2NCAyMC42NDZ6bTEwMC45OTggLTUzNi4yMzhxLTE0LjUwOCA5My4xODYgLTYxLjkzOCAzNjUuNDkgLTIuNzkgMTYuNzQgLTE1LjA2NiAzMS4yNDh0LTI0LjI3MyAyMi4zMiAtMzAuNDExIDE3LjI5OHEtMTQwLjYxNiA3MC4zMDggLTM0MC4zOCA0OS4xMDQgLTEzOC4zODQgLTE1LjA2NiAtMjE5Ljg1MiAtNzcuNTYyIC04LjM3IC02LjY5NiAtMTQuMjI5IC0xNC43ODd0LTkuNDg2IC0xOS41MyAtNS4wMjIgLTE4Ljk3MiAtMy4zNDggLTIyLjA0MSAtMy4wNjkgLTE5LjUzcS01LjAyMiAtMjcuOSAtMTQuNzg3IC04My43dC0xNS42MjQgLTkwLjExNyAtMTMuMTEzIC04Mi4zMDUgLTEyLjI3NiAtODguMTY0cTEuNjc0IC0xNC41MDggOS43NjUgLTI3LjA2M3QxNy41NzcgLTIwLjkyNSAyNS4xMSAtMTYuNzQgMjUuNjY4IC0xMi41NTUgMjYuNzg0IC0xMC4zMjNxNjkuNzUgLTI1LjY2OCAxNzQuNjU0IC0zNS43MTIgMjExLjQ4MiAtMjAuNjQ2IDM3Ny4yMDggMjcuOSA4Ni40OSAyNS42NjggMTE5Ljk3IDY4LjA3NiA4LjkyOCAxMS4xNiA5LjIwNyAyOC40NTh0LTMuMDY5IDMwLjEzMnoiIGZpbGw9IiNGRkZGRkYiLz48L3N2Zz4K); - } - .btn-text { - line-height: 36px; - padding: 6px 12px; - text-align: center; - font-weight: 600; - } - .form-row { - display: block; - margin: 20px auto; - } - label { - font-size: 13px; - font-weight: 600; - } - .input-box { - display: block; - height: 36px; - padding: 6px 12px; - font-size: 14px; - line-height: 1.42857143; - color: #666; - border: 1px solid #CCC; - border-radius: 4px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - width: 250px; - margin: auto; - } - .input-box:focus, - .input-box:active { - outline: none; - border-color: #66AFE9; - } - .error-box-field, - .error-box { - background-color: #DD1327; - max-width: 320px; - color: #fff; - font-size: 14px; - font-weight: normal; - padding: 4px 0; - } - .error-box { - margin: 20px auto; - } - .error-box-field { - margin: 0 auto; - width: 250px; - } - .instruction-block { - font-size: 14px; - } - .detail-block { - color: #777; - font-size: 12px; - margin-top: 20px; - } - .bullet-point { - list-style: square; - } - .list-with-title { - text-align: left; - margin: 0 25%; - } - .hr { - color: #999; - } - </style> - </head> - - <body> - <div id="navbar"> - <div id="navbar-logo-wrap"> - <img id="navbar-logo" src="{{ .LogoURL }}"> - </div> - </div> - - <div id="container"> - -`, - "login.html": `{{ template "header.html" . }} - -<div class="panel"> - <h2 class="heading">Log in to {{ .Issuer }} </h2> - - <div> - {{ range $c := .Connectors }} - <div class="form-row"> - <a href="{{ $c.URL }}?req={{ $.AuthReqID }}" target="_self"> - <button class="btn btn-provider"> - <span class="btn-icon btn-icon-{{ $c.ID }}"></span> - <span class="btn-text">Log in with {{ $c.Name }}</span> - </button> - </a> - </div> - {{ end }} - </div> - -</div> - - -{{ template "footer.html" . }} -`, - "oob.html": `{{ template "header.html" . }} - -<div class="panel"> - <h2 class="heading">Login Successful</h2> - - Please copy this code, switch to your application and paste it there: - <br/> - <input type="text" value="{{ .Code }}" /> -</div> - -{{ template "footer.html" . }} -`, - "password.html": `{{ template "header.html" . }} - -<div class="panel"> - <h2 class="heading">Log in to Your Account</h2> - <form method="post" action="{{ .PostURL }}"> - <div class="form-row"> - <div class="input-desc"> - <label for="userid">Username</label> - </div> - <input tabindex="1" required id="login" name="login" type="text" class="input-box" placeholder="username" {{ if .Username }}value="{{ .Username }}" {{ else }} autofocus {{ end }}/> - </div> - <div class="form-row"> - <div class="input-desc"> - <label for="password">Password</label> - </div> - <input tabindex="2" required id="password" name="password" type="password" class="input-box" placeholder="password" {{ if .Invalid }} autofocus {{ end }}/> - </div> - <input type="hidden" name="req" value="{{ .AuthReqID }}"/> - - {{ if .Invalid }} - <div class="error-box"> - Invalid username and password. - </div> - {{ end }} - - <button tabindex="3" type="submit" class="btn btn-primary">Login</button> - - </form> -</div> - -{{ template "footer.html" . }} -`, -} diff --git a/server/templates_default_gen.go b/server/templates_default_gen.go deleted file mode 100644 index 0e46df78..00000000 --- a/server/templates_default_gen.go +++ /dev/null @@ -1,85 +0,0 @@ -// +build ignore - -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "log" - "os/exec" - "path/filepath" -) - -// ignoreFile uses "git check-ignore" to determine if we should ignore a file. -func ignoreFile(p string) (ok bool, err error) { - err = exec.Command("git", "check-ignore", p).Run() - if err == nil { - return true, nil - } - exitErr, ok := err.(*exec.ExitError) - if ok { - if sys := exitErr.Sys(); sys != nil { - e, ok := sys.(interface { - // Is the returned value something that returns an exit status? - ExitStatus() int - }) - if ok && e.ExitStatus() == 1 { - return false, nil - } - } - } - return false, err -} - -// Maps aren't deterministic, use a struct instead. - -type fileData struct { - name string - data string -} - -func main() { - // ReadDir guarentees result in sorted order. - dir, err := ioutil.ReadDir("web/templates") - if err != nil { - log.Fatal(err) - } - files := []fileData{} - for _, file := range dir { - p := filepath.Join("web/templates", file.Name()) - ignore, err := ignoreFile(p) - if err != nil { - log.Fatal(err) - } - if ignore { - continue - } - - data, err := ioutil.ReadFile(p) - if err != nil { - log.Fatal(err) - } - if bytes.Contains(data, []byte{'`'}) { - log.Fatalf("file %s contains escape character '`' and cannot be compiled into go source", p) - } - files = append(files, fileData{file.Name(), string(data)}) - } - - f := new(bytes.Buffer) - - fmt.Fprintln(f, "// This file was generated by the makefile. Do not edit.") - fmt.Fprintln(f) - fmt.Fprintln(f, "package server") - fmt.Fprintln(f) - fmt.Fprintln(f, "// defaultTemplates is a key for file name to file data of the files in web/templates.") - fmt.Fprintln(f, "var defaultTemplates = map[string]string{") - for _, file := range files { - fmt.Fprintf(f, "\t%q: `%s`,\n", file.name, file.data) - } - fmt.Fprintln(f, "}") - - if err := ioutil.WriteFile("server/templates_default.go", f.Bytes(), 0644); err != nil { - log.Fatal(err) - } -} diff --git a/server/templates_test.go b/server/templates_test.go index efbb29ed..abb4e431 100644 --- a/server/templates_test.go +++ b/server/templates_test.go @@ -1,16 +1 @@ package server - -import "testing" - -func TestNewTemplates(t *testing.T) { - var config TemplateConfig - if _, err := loadTemplates(config); err != nil { - t.Fatal(err) - } -} - -func TestLoadTemplates(t *testing.T) { - var config TemplateConfig - - config.Dir = "../web/templates" -} diff --git a/web/static/main.css b/web/static/main.css new file mode 100644 index 00000000..e69de29b diff --git a/web/templates/header.html b/web/templates/header.html index cadb078d..79438ec4 100644 --- a/web/templates/header.html +++ b/web/templates/header.html @@ -3,8 +3,10 @@ <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> - <title>{{ .Issuer }}</title> + <title>{{ issuer }}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link href="{{ url "static/main.css" }}" rel="stylesheet"> + <link href="{{ url "theme/style.css" }}" rel="stylesheet"> <style> * { -webkit-box-sizing: border-box; @@ -232,7 +234,7 @@ <body> <div id="navbar"> <div id="navbar-logo-wrap"> - <img id="navbar-logo" src="{{ .LogoURL }}"> + <img id="navbar-logo" src="{{ logo }}"> </div> </div> diff --git a/web/templates/login.html b/web/templates/login.html index ea43903a..e52f775a 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -1,7 +1,7 @@ {{ template "header.html" . }} <div class="panel"> - <h2 class="heading">Log in to {{ .Issuer }} </h2> + <h2 class="heading">Log in to {{ issuer }} </h2> <div> {{ range $c := .Connectors }} diff --git a/web/themes/coreos/logo.png b/web/themes/coreos/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cf8caaaee53500d4efa77c587997788ac8d4fa6e GIT binary patch literal 2218 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 literal 0 HcmV?d00001 diff --git a/web/themes/coreos/style.css b/web/themes/coreos/style.css new file mode 100644 index 00000000..e69de29b -- GitLab