add logo to callback page
- commit
- 091d5df5efa08fdaf82831cee845684155d66b65
- author
- patriceckhart · · 2026-04-18 08:15 UTC
- parent
- d8a0cba
- signature
- unsigned
4 files · +58 -15
A internal/assets/assets.go +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | // Package assets holds static resources embedded in the zot binary. | |
| 2 | + | // Currently just the zot logo used by the tui welcome banner. | |
| 3 | + | package assets | |
| 4 | + | ||
| 5 | + | import _ "embed" | |
| 6 | + | ||
| 7 | + | // LogoPNG is the pixel-art zot `z` logo as PNG bytes. | |
| 8 | + | // Used by the interactive welcome banner; decoded once and rasterized | |
| 9 | + | // to Unicode half-blocks so it renders on any terminal without needing | |
| 10 | + | // inline image support. | |
| 11 | + | // | |
| 12 | + | //go:embed zot-logo.png | |
| 13 | + | var LogoPNG []byte | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | // Package assets holds static resources embedded in the zot binary. | |||
| 2 | + | // Currently just the zot logo used by the tui welcome banner. | |||
| 3 | + | package assets | |||
| 4 | + | ||||
| 5 | + | import _ "embed" | |||
| 6 | + | ||||
| 7 | + | // LogoPNG is the pixel-art zot `z` logo as PNG bytes. | |||
| 8 | + | // Used by the interactive welcome banner; decoded once and rasterized | |||
| 9 | + | // to Unicode half-blocks so it renders on any terminal without needing | |||
| 10 | + | // inline image support. | |||
| 11 | + | // | |||
| 12 | + | //go:embed zot-logo.png | |||
| 13 | + | var LogoPNG []byte | |||
A internal/assets/zot-logo.png +0 -0 binary file: no text diff
binary file: no text diff
M internal/auth/callback.go +40 -2
| @@ -8,6 +8,8 @@ import ( | |||
| 8 | 8 | "strings" | |
| 9 | 9 | "sync" | |
| 10 | 10 | "time" | |
| 11 | + | ||
| 12 | + | "github.com/patriceckhart/zot/internal/assets" | |
| 11 | 13 | ) | |
| 12 | 14 | ||
| 13 | 15 | // CallbackResult is what an OAuth callback server returns once the | |
| @@ -47,6 +49,7 @@ func NewCallbackServer(p OAuthProvider, expectedState string) (*CallbackServer, | |||
| 47 | 49 | } | |
| 48 | 50 | mux := http.NewServeMux() | |
| 49 | 51 | mux.HandleFunc(p.RedirectPath, cs.handle) | |
| 52 | + | mux.HandleFunc("/logo.png", serveLogo) | |
| 50 | 53 | cs.srv = &http.Server{ | |
| 51 | 54 | Handler: mux, | |
| 52 | 55 | ReadTimeout: 15 * time.Second, | |
| @@ -119,8 +122,9 @@ func (cs *CallbackServer) deliver(r CallbackResult) { | |||
| 119 | 122 | ||
| 120 | 123 | // ---- static HTML for the callback tab ---- | |
| 121 | 124 | ||
| 122 | - | // All zot-served pages share a single monochrome style: black text on | |
| 123 | - | // white, thin horizontal rules, monospace type. Matches the tui aesthetic. | |
| 125 | + | // All zot-served pages share a single style: white background, black | |
| 126 | + | // text, the cyan pixel-art `z` logo at the top, thin black rules, | |
| 127 | + | // monospace type. | |
| 124 | 128 | const monoStyle = `<style> | |
| 125 | 129 | :root { color-scheme: light; } | |
| 126 | 130 | * { box-sizing: border-box; } | |
| @@ -133,6 +137,14 @@ const monoStyle = `<style> | |||
| 133 | 137 | padding: 3rem 1.5rem; | |
| 134 | 138 | line-height: 1.55; | |
| 135 | 139 | } | |
| 140 | + | .logo { | |
| 141 | + | display: block; | |
| 142 | + | width: 120px; | |
| 143 | + | height: auto; | |
| 144 | + | image-rendering: pixelated; | |
| 145 | + | image-rendering: crisp-edges; | |
| 146 | + | margin: 0 0 1.5rem; | |
| 147 | + | } | |
| 136 | 148 | h1 { | |
| 137 | 149 | font-size: 1rem; | |
| 138 | 150 | font-weight: 600; | |
| @@ -145,11 +157,36 @@ const monoStyle = `<style> | |||
| 145 | 157 | .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; } | |
| 146 | 158 | .msg { padding: 0.75rem 0; } | |
| 147 | 159 | a { color: inherit; } | |
| 160 | + | input[type=password], input[type=text] { | |
| 161 | + | width: 100%; padding: 0.5rem 0.6rem; | |
| 162 | + | border: 1px solid #000000; background: #ffffff; color: #000000; | |
| 163 | + | font-family: inherit; font-size: 0.95rem; | |
| 164 | + | } | |
| 165 | + | button { | |
| 166 | + | padding: 0.5rem 1.25rem; | |
| 167 | + | background: #000000; color: #ffffff; | |
| 168 | + | border: 1px solid #000000; font-family: inherit; font-size: 0.95rem; | |
| 169 | + | cursor: pointer; | |
| 170 | + | } | |
| 171 | + | button:hover { background: #ffffff; color: #000000; } | |
| 148 | 172 | </style>` | |
| 149 | 173 | ||
| 174 | + | // logoTag is the <img> element used at the top of every zot-served | |
| 175 | + | // page. The image bytes are served from /logo.png by the same server. | |
| 176 | + | const logoTag = `<img class="logo" src="/logo.png" alt="zot" />` | |
| 177 | + | ||
| 178 | + | // serveLogo writes the embedded PNG with appropriate caching headers. | |
| 179 | + | // Shared between the oauth callback server and the api-key login server. | |
| 180 | + | func serveLogo(w http.ResponseWriter, r *http.Request) { | |
| 181 | + | w.Header().Set("content-type", "image/png") | |
| 182 | + | w.Header().Set("cache-control", "public, max-age=86400") | |
| 183 | + | _, _ = w.Write(assets.LogoPNG) | |
| 184 | + | } | |
| 185 | + | ||
| 150 | 186 | func oauthSuccessHTML(provider string) string { | |
| 151 | 187 | p := strings.ToLower(provider) | |
| 152 | 188 | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | |
| 189 | + | ` + logoTag + ` | |
| 153 | 190 | <h1><span class="mark">✓</span> logged in to ` + p + `</h1> | |
| 154 | 191 | <hr class="rule"> | |
| 155 | 192 | <p class="msg">zot received the callback. you can close this tab.</p> | |
| @@ -158,6 +195,7 @@ func oauthSuccessHTML(provider string) string { | |||
| 158 | 195 | ||
| 159 | 196 | func oauthErrorHTML(msg string) string { | |
| 160 | 197 | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | |
| 198 | + | ` + logoTag + ` | |
| 161 | 199 | <h1><span class="mark">✗</span> login failed</h1> | |
| 162 | 200 | <hr class="rule"> | |
| 163 | 201 | <p class="msg mono">` + htmlEscape(msg) + `</p> | |
| @@ -8,6 +8,8 @@ import ( | |||||
| 8 | "strings" | 8 | "strings" | ||
| 9 | "sync" | 9 | "sync" | ||
| 10 | "time" | 10 | "time" | ||
| 11 | + | ||||
| 12 | + | "github.com/patriceckhart/zot/internal/assets" | |||
| 11 | ) | 13 | ) | ||
| 12 | 14 | ||||
| 13 | // CallbackResult is what an OAuth callback server returns once the | 15 | // CallbackResult is what an OAuth callback server returns once the | ||
| @@ -47,6 +49,7 @@ func NewCallbackServer(p OAuthProvider, expectedState string) (*CallbackServer, | |||||
| 47 | } | 49 | } | ||
| 48 | mux := http.NewServeMux() | 50 | mux := http.NewServeMux() | ||
| 49 | mux.HandleFunc(p.RedirectPath, cs.handle) | 51 | mux.HandleFunc(p.RedirectPath, cs.handle) | ||
| 52 | + | mux.HandleFunc("/logo.png", serveLogo) | |||
| 50 | cs.srv = &http.Server{ | 53 | cs.srv = &http.Server{ | ||
| 51 | Handler: mux, | 54 | Handler: mux, | ||
| 52 | ReadTimeout: 15 * time.Second, | 55 | ReadTimeout: 15 * time.Second, | ||
| @@ -119,8 +122,9 @@ func (cs *CallbackServer) deliver(r CallbackResult) { | |||||
| 119 | 122 | ||||
| 120 | // ---- static HTML for the callback tab ---- | 123 | // ---- static HTML for the callback tab ---- | ||
| 121 | 124 | ||||
| 122 | - | // All zot-served pages share a single monochrome style: black text on | 125 | + | // All zot-served pages share a single style: white background, black |
| 123 | - | // white, thin horizontal rules, monospace type. Matches the tui aesthetic. | 126 | + | // text, the cyan pixel-art `z` logo at the top, thin black rules, |
| 127 | + | // monospace type. | |||
| 124 | const monoStyle = `<style> | 128 | const monoStyle = `<style> | ||
| 125 | :root { color-scheme: light; } | 129 | :root { color-scheme: light; } | ||
| 126 | * { box-sizing: border-box; } | 130 | * { box-sizing: border-box; } | ||
| @@ -133,6 +137,14 @@ const monoStyle = `<style> | |||||
| 133 | padding: 3rem 1.5rem; | 137 | padding: 3rem 1.5rem; | ||
| 134 | line-height: 1.55; | 138 | line-height: 1.55; | ||
| 135 | } | 139 | } | ||
| 140 | + | .logo { | |||
| 141 | + | display: block; | |||
| 142 | + | width: 120px; | |||
| 143 | + | height: auto; | |||
| 144 | + | image-rendering: pixelated; | |||
| 145 | + | image-rendering: crisp-edges; | |||
| 146 | + | margin: 0 0 1.5rem; | |||
| 147 | + | } | |||
| 136 | h1 { | 148 | h1 { | ||
| 137 | font-size: 1rem; | 149 | font-size: 1rem; | ||
| 138 | font-weight: 600; | 150 | font-weight: 600; | ||
| @@ -145,11 +157,36 @@ const monoStyle = `<style> | |||||
| 145 | .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; } | 157 | .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; } | ||
| 146 | .msg { padding: 0.75rem 0; } | 158 | .msg { padding: 0.75rem 0; } | ||
| 147 | a { color: inherit; } | 159 | a { color: inherit; } | ||
| 160 | + | input[type=password], input[type=text] { | |||
| 161 | + | width: 100%; padding: 0.5rem 0.6rem; | |||
| 162 | + | border: 1px solid #000000; background: #ffffff; color: #000000; | |||
| 163 | + | font-family: inherit; font-size: 0.95rem; | |||
| 164 | + | } | |||
| 165 | + | button { | |||
| 166 | + | padding: 0.5rem 1.25rem; | |||
| 167 | + | background: #000000; color: #ffffff; | |||
| 168 | + | border: 1px solid #000000; font-family: inherit; font-size: 0.95rem; | |||
| 169 | + | cursor: pointer; | |||
| 170 | + | } | |||
| 171 | + | button:hover { background: #ffffff; color: #000000; } | |||
| 148 | </style>` | 172 | </style>` | ||
| 149 | 173 | ||||
| 174 | + | // logoTag is the <img> element used at the top of every zot-served | |||
| 175 | + | // page. The image bytes are served from /logo.png by the same server. | |||
| 176 | + | const logoTag = `<img class="logo" src="/logo.png" alt="zot" />` | |||
| 177 | + | ||||
| 178 | + | // serveLogo writes the embedded PNG with appropriate caching headers. | |||
| 179 | + | // Shared between the oauth callback server and the api-key login server. | |||
| 180 | + | func serveLogo(w http.ResponseWriter, r *http.Request) { | |||
| 181 | + | w.Header().Set("content-type", "image/png") | |||
| 182 | + | w.Header().Set("cache-control", "public, max-age=86400") | |||
| 183 | + | _, _ = w.Write(assets.LogoPNG) | |||
| 184 | + | } | |||
| 185 | + | ||||
| 150 | func oauthSuccessHTML(provider string) string { | 186 | func oauthSuccessHTML(provider string) string { | ||
| 151 | p := strings.ToLower(provider) | 187 | p := strings.ToLower(provider) | ||
| 152 | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | 188 | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | ||
| 189 | + | ` + logoTag + ` | |||
| 153 | <h1><span class="mark">✓</span> logged in to ` + p + `</h1> | 190 | <h1><span class="mark">✓</span> logged in to ` + p + `</h1> | ||
| 154 | <hr class="rule"> | 191 | <hr class="rule"> | ||
| 155 | <p class="msg">zot received the callback. you can close this tab.</p> | 192 | <p class="msg">zot received the callback. you can close this tab.</p> | ||
| @@ -158,6 +195,7 @@ func oauthSuccessHTML(provider string) string { | |||||
| 158 | 195 | ||||
| 159 | func oauthErrorHTML(msg string) string { | 196 | func oauthErrorHTML(msg string) string { | ||
| 160 | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | 197 | return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | ||
| 198 | + | ` + logoTag + ` | |||
| 161 | <h1><span class="mark">✗</span> login failed</h1> | 199 | <h1><span class="mark">✗</span> login failed</h1> | ||
| 162 | <hr class="rule"> | 200 | <hr class="rule"> | ||
| 163 | <p class="msg mono">` + htmlEscape(msg) + `</p> | 201 | <p class="msg mono">` + htmlEscape(msg) + `</p> | ||
M internal/auth/server.go +5 -13
| @@ -62,6 +62,7 @@ func NewServer() (*Server, error) { | |||
| 62 | 62 | mux.HandleFunc("/callback", s.handleCallback) | |
| 63 | 63 | mux.HandleFunc("/success", s.handleSuccess) | |
| 64 | 64 | mux.HandleFunc("/error", s.handleError) | |
| 65 | + | mux.HandleFunc("/logo.png", serveLogo) | |
| 65 | 66 | s.srv = &http.Server{ | |
| 66 | 67 | Handler: mux, | |
| 67 | 68 | ReadTimeout: 15 * time.Second, | |
| @@ -219,6 +220,7 @@ func Redirect(w http.ResponseWriter, r *http.Request, u *url.URL) { | |||
| 219 | 220 | // rules, no rounded boxes, no color. | |
| 220 | 221 | ||
| 221 | 222 | var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body> | |
| 223 | + | ` + logoTag + ` | |
| 222 | 224 | <h1>zot login</h1> | |
| 223 | 225 | <hr class="rule"> | |
| 224 | 226 | <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> | |
| @@ -234,20 +236,8 @@ func init() { | |||
| 234 | 236 | template.Must(tpl.New("apikey").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `<style> | |
| 235 | 237 | form { display: flex; flex-direction: column; gap: 0.75rem; } | |
| 236 | 238 | label { font-size: 0.875rem; } | |
| 237 | - | input[type=password] { | |
| 238 | - | width: 100%; padding: 0.5rem 0.6rem; | |
| 239 | - | border: 1px solid #000000; background: #ffffff; color: #000000; | |
| 240 | - | font-family: inherit; font-size: 0.95rem; | |
| 241 | - | } | |
| 242 | - | button { | |
| 243 | - | align-self: flex-start; | |
| 244 | - | padding: 0.5rem 1.25rem; | |
| 245 | - | background: #000000; color: #ffffff; | |
| 246 | - | border: 1px solid #000000; font-family: inherit; font-size: 0.95rem; | |
| 247 | - | cursor: pointer; | |
| 248 | - | } | |
| 249 | - | button:hover { background: #ffffff; color: #000000; } | |
| 250 | 239 | </style></head><body> | |
| 240 | + | ` + logoTag + ` | |
| 251 | 241 | <h1>zot login · {{.Provider}} api key</h1> | |
| 252 | 242 | <hr class="rule"> | |
| 253 | 243 | <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p> | |
| @@ -260,6 +250,7 @@ func init() { | |||
| 260 | 250 | </body></html>`)) | |
| 261 | 251 | ||
| 262 | 252 | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | |
| 253 | + | ` + logoTag + ` | |
| 263 | 254 | <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1> | |
| 264 | 255 | <hr class="rule"> | |
| 265 | 256 | <p class="msg">method: {{.Method}}</p> | |
| @@ -267,6 +258,7 @@ func init() { | |||
| 267 | 258 | </body></html>`)) | |
| 268 | 259 | ||
| 269 | 260 | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | |
| 261 | + | ` + logoTag + ` | |
| 270 | 262 | <h1><span class="mark">✗</span> login failed</h1> | |
| 271 | 263 | <hr class="rule"> | |
| 272 | 264 | <p class="msg mono">{{.Message}}</p> | |
| @@ -62,6 +62,7 @@ func NewServer() (*Server, error) { | |||||
| 62 | mux.HandleFunc("/callback", s.handleCallback) | 62 | mux.HandleFunc("/callback", s.handleCallback) | ||
| 63 | mux.HandleFunc("/success", s.handleSuccess) | 63 | mux.HandleFunc("/success", s.handleSuccess) | ||
| 64 | mux.HandleFunc("/error", s.handleError) | 64 | mux.HandleFunc("/error", s.handleError) | ||
| 65 | + | mux.HandleFunc("/logo.png", serveLogo) | |||
| 65 | s.srv = &http.Server{ | 66 | s.srv = &http.Server{ | ||
| 66 | Handler: mux, | 67 | Handler: mux, | ||
| 67 | ReadTimeout: 15 * time.Second, | 68 | ReadTimeout: 15 * time.Second, | ||
| @@ -219,6 +220,7 @@ func Redirect(w http.ResponseWriter, r *http.Request, u *url.URL) { | |||||
| 219 | // rules, no rounded boxes, no color. | 220 | // rules, no rounded boxes, no color. | ||
| 220 | 221 | ||||
| 221 | var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body> | 222 | var tpl = template.Must(template.New("index").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `</head><body> | ||
| 223 | + | ` + logoTag + ` | |||
| 222 | <h1>zot login</h1> | 224 | <h1>zot login</h1> | ||
| 223 | <hr class="rule"> | 225 | <hr class="rule"> | ||
| 224 | <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> | 226 | <p>paste an api key for anthropic or openai. zot probes the provider once, then saves the key to <span class="mono">~/Library/Application Support/zot/auth.json</span>.</p> | ||
| @@ -234,20 +236,8 @@ func init() { | |||||
| 234 | template.Must(tpl.New("apikey").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `<style> | 236 | template.Must(tpl.New("apikey").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot login</title>` + monoStyle + `<style> | ||
| 235 | form { display: flex; flex-direction: column; gap: 0.75rem; } | 237 | form { display: flex; flex-direction: column; gap: 0.75rem; } | ||
| 236 | label { font-size: 0.875rem; } | 238 | label { font-size: 0.875rem; } | ||
| 237 | - | input[type=password] { | |||
| 238 | - | width: 100%; padding: 0.5rem 0.6rem; | |||
| 239 | - | border: 1px solid #000000; background: #ffffff; color: #000000; | |||
| 240 | - | font-family: inherit; font-size: 0.95rem; | |||
| 241 | - | } | |||
| 242 | - | button { | |||
| 243 | - | align-self: flex-start; | |||
| 244 | - | padding: 0.5rem 1.25rem; | |||
| 245 | - | background: #000000; color: #ffffff; | |||
| 246 | - | border: 1px solid #000000; font-family: inherit; font-size: 0.95rem; | |||
| 247 | - | cursor: pointer; | |||
| 248 | - | } | |||
| 249 | - | button:hover { background: #ffffff; color: #000000; } | |||
| 250 | </style></head><body> | 239 | </style></head><body> | ||
| 240 | + | ` + logoTag + ` | |||
| 251 | <h1>zot login · {{.Provider}} api key</h1> | 241 | <h1>zot login · {{.Provider}} api key</h1> | ||
| 252 | <hr class="rule"> | 242 | <hr class="rule"> | ||
| 253 | <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p> | 243 | <p>paste your {{.Provider}} api key. zot will probe the provider with it once, then save it if the key is accepted.</p> | ||
| @@ -260,6 +250,7 @@ func init() { | |||||
| 260 | </body></html>`)) | 250 | </body></html>`)) | ||
| 261 | 251 | ||||
| 262 | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | 252 | template.Must(tpl.New("success").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · logged in</title>` + monoStyle + `</head><body> | ||
| 253 | + | ` + logoTag + ` | |||
| 263 | <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1> | 254 | <h1><span class="mark">✓</span> logged in to {{.Provider}}</h1> | ||
| 264 | <hr class="rule"> | 255 | <hr class="rule"> | ||
| 265 | <p class="msg">method: {{.Method}}</p> | 256 | <p class="msg">method: {{.Method}}</p> | ||
| @@ -267,6 +258,7 @@ func init() { | |||||
| 267 | </body></html>`)) | 258 | </body></html>`)) | ||
| 268 | 259 | ||||
| 269 | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | 260 | template.Must(tpl.New("error").Parse(`<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>zot · error</title>` + monoStyle + `</head><body> | ||
| 261 | + | ` + logoTag + ` | |||
| 270 | <h1><span class="mark">✗</span> login failed</h1> | 262 | <h1><span class="mark">✗</span> login failed</h1> | ||
| 271 | <hr class="rule"> | 263 | <hr class="rule"> | ||
| 272 | <p class="msg mono">{{.Message}}</p> | 264 | <p class="msg mono">{{.Message}}</p> | ||