diff --git a/examples/example-app/templates.go b/examples/example-app/templates.go index a9425ead2761e0a08e65e3e598e6b6f1748b7a51..7107eb87394999b93fc5371f39e4f707c0f5a7d4 100644 --- a/examples/example-app/templates.go +++ b/examples/example-app/templates.go @@ -6,38 +6,225 @@ import ( "net/http" ) +const css = ` + body { + font-family: Arial, sans-serif; + background-color: #f2f2f2; + margin: 0; + } + + .header { + text-align: center; + margin-bottom: 20px; + } + + .dex { + font-size: 2em; + font-weight: bold; + color: #3F9FD8; /* Main color */ + } + + .example-app { + font-size: 1em; + color: #EF4B5C; /* Secondary color */ + } + + .form-instructions { + text-align: center; + margin-bottom: 15px; + font-size: 1em; + color: #555; + } + + hr { + border: none; + border-top: 1px solid #ccc; + margin-top: 10px; + margin-bottom: 20px; + } + + label { + flex: 1; + font-weight: bold; + color: #333; + } + + p { + margin-bottom: 15px; + display: flex; + align-items: center; + } + + input[type="text"] { + flex: 2; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + outline: none; + } + + input[type="checkbox"] { + margin-left: 10px; + transform: scale(1.2); + } + + .back-button { + display: inline-block; + padding: 8px 16px; + background-color: #EF4B5C; /* Secondary color */ + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: fixed; + right: 20px; + bottom: 20px; + } + + .back-button:hover { + background-color: #C43B4B; /* Darker shade of secondary color */ + } + + .token-block { + background-color: #fff; + padding: 10px 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 15px; + word-wrap: break-word; + display: flex; + flex-direction: column; + gap: 5px; + position: relative; + } + + .token-title { + font-weight: bold; + display: flex; + justify-content: space-between; + align-items: center; + } + + .token-title a { + font-size: 0.9em; + text-decoration: none; + color: #3F9FD8; /* Main color */ + } + + .token-title a:hover { + text-decoration: underline; + } + + .token-code { + overflow-wrap: break-word; + word-break: break-all; + white-space: normal; + } + + pre { + white-space: pre-wrap; + background-color: #f9f9f9; + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; + margin: 0; + font-family: 'Courier New', Courier, monospace; + overflow-x: auto; + font-size: 0.9em; + position: relative; + margin-top: 5px; + } + + pre .key { + color: #c00; + } + + pre .string { + color: #080; + } + + pre .number { + color: #00f; + } +` + var indexTmpl = template.Must(template.New("index.html").Parse(`<html> - <head> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Example App - Login</title> <style> -form { display: table; } -p { display: table-row; } -label { display: table-cell; } -input { display: table-cell; } +` + css + ` + body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + flex-direction: column; + } + + form { + background-color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; + } + + input[type="submit"] { + width: 100%; + padding: 10px; + background-color: #3F9FD8; /* Main color */ + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + } + + input[type="submit"]:hover { + background-color: #357FAA; /* Darker shade of main color */ + } </style> - </head> - <body> +</head> +<body> + <div class="header"> + <div class="dex">Dex</div> + <div class="example-app">Example App</div> + </div> <form action="/login" method="post"> - <p> - <label> Authenticate for: </label> - <input type="text" name="cross_client" placeholder="list of client-ids"> - </p> - <p> - <label>Extra scopes: </label> - <input type="text" name="extra_scopes" placeholder="list of scopes"> - </p> - <p> - <label>Connector ID: </label> - <input type="text" name="connector_id" placeholder="connector id"> - </p> - <p> - <label>Request offline access: </label> - <input type="checkbox" name="offline_access" value="yes" checked> - </p> - <p> - <input type="submit" value="Login"> - </p> + <div class="form-instructions"> + If needed, customize your login settings below, then click <strong>Login</strong> to proceed. + </div> + <hr/> + <p> + <label for="cross_client">Authenticate for:</label> + <input type="text" id="cross_client" name="cross_client" placeholder="list of client-ids"> + </p> + <p> + <label for="extra_scopes">Extra scopes:</label> + <input type="text" id="extra_scopes" name="extra_scopes" placeholder="list of scopes"> + </p> + <p> + <label for="connector_id">Connector ID:</label> + <input type="text" id="connector_id" name="connector_id" placeholder="connector id"> + </p> + <p> + <label for="offline_access">Request offline access:</label> + <input type="checkbox" id="offline_access" name="offline_access" value="yes" checked> + </p> + <p> + <input type="submit" value="Login"> + </p> </form> - </body> +</body> </html>`)) func renderIndex(w http.ResponseWriter) { @@ -53,30 +240,116 @@ type tokenTmplData struct { } var tokenTmpl = template.Must(template.New("token.html").Parse(`<html> - <head> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Tokens</title> <style> -/* make pre wrap */ -pre { - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ -} +` + css + ` + body { + color: #333; + margin: 0; + padding: 20px; + position: relative; + } + + input[type="submit"] { + margin-top: 10px; + padding: 8px 16px; + background-color: #3F9FD8; /* Main color */ + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s ease; + } + + input[type="submit"]:hover { + background-color: #357FAA; /* Darker shade of main color */ + } </style> - </head> - <body> - <p> ID Token: <pre><code>{{ .IDToken }}</code></pre></p> - <p> Access Token: <pre><code>{{ .AccessToken }}</code></pre></p> - <p> Claims: <pre><code>{{ .Claims }}</code></pre></p> - {{ if .RefreshToken }} - <p> Refresh Token: <pre><code>{{ .RefreshToken }}</code></pre></p> - <form action="{{ .RedirectURL }}" method="post"> - <input type="hidden" name="refresh_token" value="{{ .RefreshToken }}"> - <input type="submit" value="Redeem refresh token"> - </form> - {{ end }} - </body> +</head> +<body> + {{ if .IDToken }} + <div class="token-block"> + <div class="token-title"> + ID Token: + <a href="#" onclick="window.open('https://jwt.io/#debugger-io?token=' + encodeURIComponent('{{ .IDToken }}'), '_blank')">Decode on jwt.io</a> + </div> + <pre><code class="token-code">{{ .IDToken }}</code></pre> + </div> + {{ end }} + + {{ if .AccessToken }} + <div class="token-block"> + <div class="token-title"> + Access Token: + <a href="#" onclick="window.open('https://jwt.io/#debugger-io?token=' + encodeURIComponent('{{ .AccessToken }}'), '_blank')">Decode on jwt.io</a> + </div> + <pre><code class="token-code">{{ .AccessToken }}</code></pre> + </div> + {{ end }} + + {{ if .Claims }} + <div class="token-block"> + <div class="token-title">Claims:</div> + <pre><code id="claims">{{ .Claims }}</code></pre> + </div> + {{ end }} + + {{ if .RefreshToken }} + <div class="token-block"> + <div class="token-title">Refresh Token:</div> + <pre><code class="token-code">{{ .RefreshToken }}</code></pre> + <form action="{{ .RedirectURL }}" method="post"> + <input type="hidden" name="refresh_token" value="{{ .RefreshToken }}"> + <input type="submit" value="Redeem refresh token"> + </form> + </div> + {{ end }} + + <a href="/" class="back-button">Back to Home</a> + + <script> + // Simple JSON syntax highlighter + document.addEventListener("DOMContentLoaded", function() { + const claimsElement = document.getElementById("claims"); + if (claimsElement) { + try { + const json = JSON.parse(claimsElement.textContent); + claimsElement.innerHTML = syntaxHighlight(json); + } catch (e) { + console.error("Invalid JSON in claims:", e); + } + } + }); + + function syntaxHighlight(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, undefined, 2); + } + json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); + return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g, function (match) { + let cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '<span class="' + cls + '">' + match + '</span>'; + }); + } + </script> +</body> </html> `))