diff --git a/Dockerfile b/Dockerfile index c53616677df17d0321b1f84719917a86fa20f67e..4f0d43a4d3d3dc6f6ea2f1bf4fa241df3babf5d1 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 62ab589529a283b8298ea3a536d0dd0de8e90761..5aa8144e0fd57e53b2be1ae4a69b26e7c41d0755 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 2ee9e58ae2480e4516ea97295bf108b3eab636c7..dc3715b8f3ca3a2c4db48049845a404af201036a 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 e218e4732c90b6ff8c801aa2b71a00818e73aed7..c9baa9d38ce6a0de4ad05812c3404c6eb103c3e1 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 5f94e2023709753bbf3b98564d1b1b664e6d6390..1b5a48d3f15cf0eb498548b6ea32e0fd2561187b 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 b8d2c8d395d9295163bfb1d3a2616e79a19f9621..91d119e9fda8b06189eee939bbf3f3265bf9e911 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 a5865dfa33e1a7b3828222e7edc3b580d5cda65e..3ab41940826a8352990a9ee418959f31a02cc87f 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 e8285fe31e26b579b076cc430289ec8fe1f2b10b..649d5b0837e3a4ab15da1f1f253078846bf6f2de 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 651c7411b8c9eaa9a7f26fae82aa59533ad31427..0000000000000000000000000000000000000000 --- 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 0e46df780e9e1a3d8446cff1947f89f303913321..0000000000000000000000000000000000000000 --- 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 efbb29edf9ad03f12a894f8c3a6bb2f4138798d0..abb4e431abd516750a5a1e5e2b77073c236b8f9e 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/web/templates/header.html b/web/templates/header.html index cadb078d9750f0b1aab2b81a82eb3b85f60096d6..79438ec44e2a0927282ee56adfe8be002f7e20bf 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 ea43903a538e55b652babf21dc4f0ec722b6dd58..e52f775ac6979a12c945ddfb562a941e4e1584f0 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 Binary files /dev/null and b/web/themes/coreos/logo.png differ diff --git a/web/themes/coreos/style.css b/web/themes/coreos/style.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391