mirror of https://github.com/dexidp/dex.git
Browse Source
This is a preparation for KubeCon 2026: 1. Restyle the app 2. Refactor advanced configuration options 3. Move embedded templates and css to separate files Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>pull/4569/head
8 changed files with 863 additions and 343 deletions
@ -0,0 +1,106 @@
|
||||
(function() { |
||||
const crossClientInput = document.getElementById("cross_client_input"); |
||||
const crossClientList = document.getElementById("cross-client-list"); |
||||
const addClientBtn = document.getElementById("add-cross-client"); |
||||
const scopesList = document.getElementById("scopes-list"); |
||||
const customScopeInput = document.getElementById("custom_scope_input"); |
||||
const addCustomScopeBtn = document.getElementById("add-custom-scope"); |
||||
|
||||
// Default scopes that should be checked by default
|
||||
const defaultScopes = ["openid", "profile", "email", "offline_access"]; |
||||
|
||||
// Check default scopes on page load
|
||||
document.addEventListener("DOMContentLoaded", function() { |
||||
const checkboxes = scopesList.querySelectorAll('input[type="checkbox"]'); |
||||
checkboxes.forEach(cb => { |
||||
if (defaultScopes.includes(cb.value)) { |
||||
cb.checked = true; |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
function addCrossClient(value) { |
||||
const trimmed = value.trim(); |
||||
if (!trimmed) return; |
||||
|
||||
const chip = document.createElement("div"); |
||||
chip.className = "chip"; |
||||
|
||||
const text = document.createElement("span"); |
||||
text.textContent = trimmed; |
||||
|
||||
const hidden = document.createElement("input"); |
||||
hidden.type = "hidden"; |
||||
hidden.name = "cross_client"; |
||||
hidden.value = trimmed; |
||||
|
||||
const remove = document.createElement("button"); |
||||
remove.type = "button"; |
||||
remove.textContent = "×"; |
||||
remove.onclick = () => crossClientList.removeChild(chip); |
||||
|
||||
chip.append(text, hidden, remove); |
||||
crossClientList.appendChild(chip); |
||||
} |
||||
|
||||
function addCustomScope(scope) { |
||||
const trimmed = scope.trim(); |
||||
if (!trimmed || !scopesList) return; |
||||
|
||||
// Check if scope already exists
|
||||
const existingCheckboxes = scopesList.querySelectorAll('input[type="checkbox"]'); |
||||
for (const cb of existingCheckboxes) { |
||||
if (cb.value === trimmed) { |
||||
cb.checked = true; |
||||
return; |
||||
} |
||||
} |
||||
|
||||
// Add new scope checkbox
|
||||
const scopeItem = document.createElement("div"); |
||||
scopeItem.className = "scope-item"; |
||||
|
||||
const checkbox = document.createElement("input"); |
||||
checkbox.type = "checkbox"; |
||||
checkbox.name = "extra_scopes"; |
||||
checkbox.value = trimmed; |
||||
checkbox.id = "scope_custom_" + trimmed; |
||||
checkbox.checked = true; |
||||
|
||||
const label = document.createElement("label"); |
||||
label.htmlFor = checkbox.id; |
||||
label.textContent = trimmed; |
||||
|
||||
scopeItem.append(checkbox, label); |
||||
scopesList.appendChild(scopeItem); |
||||
} |
||||
|
||||
addClientBtn?.addEventListener("click", () => { |
||||
addCrossClient(crossClientInput.value); |
||||
crossClientInput.value = ""; |
||||
crossClientInput.focus(); |
||||
}); |
||||
|
||||
crossClientInput?.addEventListener("keydown", (e) => { |
||||
if (e.key === "Enter") { |
||||
e.preventDefault(); |
||||
addCrossClient(crossClientInput.value); |
||||
crossClientInput.value = ""; |
||||
} |
||||
}); |
||||
|
||||
addCustomScopeBtn?.addEventListener("click", () => { |
||||
addCustomScope(customScopeInput.value); |
||||
customScopeInput.value = ""; |
||||
customScopeInput.focus(); |
||||
}); |
||||
|
||||
customScopeInput?.addEventListener("keydown", (e) => { |
||||
if (e.key === "Enter") { |
||||
e.preventDefault(); |
||||
addCustomScope(customScopeInput.value); |
||||
customScopeInput.value = ""; |
||||
} |
||||
}); |
||||
})(); |
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,357 @@
|
||||
body { |
||||
font-family: Arial, sans-serif; |
||||
background-color: #f2f2f2; |
||||
margin: 0; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
min-height: 100vh; |
||||
flex-direction: column; |
||||
padding: 20px; |
||||
} |
||||
|
||||
/* Token page layout - no centering */ |
||||
body.token-page { |
||||
display: block; |
||||
align-items: flex-start; |
||||
justify-content: flex-start; |
||||
padding: 20px; |
||||
} |
||||
|
||||
.container { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
width: 100%; |
||||
max-width: 600px; |
||||
} |
||||
|
||||
.logo { |
||||
max-width: 100%; |
||||
width: 200px; |
||||
height: auto; |
||||
margin-bottom: 30px; |
||||
display: block; |
||||
} |
||||
|
||||
form { |
||||
background-color: #fff; |
||||
padding: 30px; |
||||
border-radius: 8px; |
||||
width: 100%; |
||||
} |
||||
|
||||
/* Shadow only for main login form */ |
||||
.container form { |
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.primary-action { |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
.advanced { |
||||
margin-top: 20px; |
||||
background: #f9fbfd; |
||||
border: 1px solid #dbe7f3; |
||||
border-radius: 6px; |
||||
padding: 15px; |
||||
} |
||||
|
||||
.advanced summary { |
||||
cursor: pointer; |
||||
font-weight: bold; |
||||
color: #3F9FD8; |
||||
user-select: none; |
||||
text-align: center; |
||||
} |
||||
|
||||
.advanced summary:hover { |
||||
color: #357FAA; |
||||
} |
||||
|
||||
.app-description { |
||||
text-align: center; |
||||
color: #666; |
||||
font-size: 14px; |
||||
margin-bottom: 25px; |
||||
line-height: 1.5; |
||||
} |
||||
|
||||
.app-description a { |
||||
color: #3F9FD8; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
.app-description a:hover { |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
.field { |
||||
margin-top: 15px; |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.field label { |
||||
font-weight: 600; |
||||
color: #333; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.inline-input { |
||||
display: flex; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.inline-input input[type="text"] { |
||||
flex: 1; |
||||
} |
||||
|
||||
input[type="text"] { |
||||
padding: 10px 12px; |
||||
border: 1px solid #ddd; |
||||
border-radius: 4px; |
||||
font-size: 14px; |
||||
outline: none; |
||||
transition: border-color 0.2s; |
||||
} |
||||
|
||||
input[type="text"]:focus { |
||||
border-color: #3F9FD8; |
||||
} |
||||
|
||||
.checkbox-field { |
||||
flex-direction: row; |
||||
align-items: center; |
||||
gap: 8px; |
||||
margin-top: 10px; |
||||
} |
||||
|
||||
.checkbox-field label { |
||||
margin: 0; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
input[type="checkbox"] { |
||||
width: 18px; |
||||
height: 18px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.scopes-list { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 8px; |
||||
max-height: 200px; |
||||
overflow-y: auto; |
||||
padding: 10px; |
||||
background: white; |
||||
border: 1px solid #ddd; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.scope-item { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.scope-item input[type="checkbox"] { |
||||
margin: 0; |
||||
} |
||||
|
||||
.scope-item label { |
||||
margin: 0; |
||||
font-weight: 400; |
||||
font-size: 13px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
input[type="submit"], button { |
||||
padding: 12px 20px; |
||||
background-color: #3F9FD8; |
||||
color: white; |
||||
border: none; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
transition: background-color 0.2s; |
||||
} |
||||
|
||||
/* Full width submit button only on main login form */ |
||||
form input[type="submit"] { |
||||
width: 100%; |
||||
font-size: 16px; |
||||
} |
||||
|
||||
/* Token page forms should have auto-width buttons */ |
||||
body.token-page input[type="submit"] { |
||||
width: auto; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
input[type="submit"]:hover, button:hover { |
||||
background-color: #357FAA; |
||||
} |
||||
|
||||
button { |
||||
white-space: nowrap; |
||||
} |
||||
|
||||
.chip-list { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
gap: 8px; |
||||
min-height: 32px; |
||||
margin-top: 8px; |
||||
} |
||||
|
||||
.chip { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
gap: 6px; |
||||
background: #e9f4fb; |
||||
border: 1px solid #cde5f5; |
||||
border-radius: 16px; |
||||
padding: 6px 12px; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.chip button { |
||||
border: none; |
||||
background: transparent; |
||||
cursor: pointer; |
||||
font-weight: bold; |
||||
color: #3F9FD8; |
||||
padding: 0; |
||||
width: 20px; |
||||
height: 20px; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border-radius: 50%; |
||||
} |
||||
|
||||
.chip button:hover { |
||||
background: #d0e8f5; |
||||
} |
||||
|
||||
.hint { |
||||
font-size: 12px; |
||||
color: #666; |
||||
margin-top: 4px; |
||||
} |
||||
|
||||
.copy-btn { |
||||
padding: 4px 10px; |
||||
background-color: #3F9FD8; |
||||
color: white; |
||||
border: none; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
font-size: 11px; |
||||
transition: background-color 0.2s; |
||||
margin-left: 8px; |
||||
white-space: nowrap; |
||||
} |
||||
|
||||
.copy-btn:hover { |
||||
background-color: #357FAA; |
||||
} |
||||
|
||||
/* Token page styles */ |
||||
.back-button { |
||||
display: inline-block; |
||||
padding: 8px 16px; |
||||
background-color: #EF4B5C; |
||||
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; |
||||
} |
||||
|
||||
.token-block { |
||||
background-color: #fff; |
||||
padding: 10px 15px; |
||||
border-radius: 8px; |
||||
margin-bottom: 15px; |
||||
word-wrap: break-word; |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 5px; |
||||
position: relative; |
||||
overflow: hidden; |
||||
border: 1px solid #e0e0e0; |
||||
} |
||||
|
||||
.token-block form { |
||||
margin: 10px 0 0 0; |
||||
padding: 0; |
||||
background-color: transparent; |
||||
box-shadow: none; |
||||
border-radius: 0; |
||||
} |
||||
|
||||
.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; |
||||
} |
||||
|
||||
.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; |
||||
} |
||||
|
||||
@ -0,0 +1,82 @@
|
||||
// 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>'; |
||||
}); |
||||
} |
||||
|
||||
function copyPublicKey() { |
||||
const publicKeyElement = document.getElementById("public-key"); |
||||
if (!publicKeyElement) return; |
||||
|
||||
const text = publicKeyElement.textContent; |
||||
|
||||
// Use modern clipboard API if available
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) { |
||||
navigator.clipboard.writeText(text).then(() => { |
||||
showCopyFeedback("Copied!"); |
||||
}).catch(err => { |
||||
console.error("Failed to copy:", err); |
||||
fallbackCopy(text); |
||||
}); |
||||
} else { |
||||
fallbackCopy(text); |
||||
} |
||||
} |
||||
|
||||
function fallbackCopy(text) { |
||||
const textarea = document.createElement("textarea"); |
||||
textarea.value = text; |
||||
textarea.style.position = "fixed"; |
||||
textarea.style.opacity = "0"; |
||||
document.body.appendChild(textarea); |
||||
textarea.select(); |
||||
try { |
||||
document.execCommand("copy"); |
||||
showCopyFeedback("Copied!"); |
||||
} catch (err) { |
||||
console.error("Fallback copy failed:", err); |
||||
showCopyFeedback("Failed to copy"); |
||||
} |
||||
document.body.removeChild(textarea); |
||||
} |
||||
|
||||
function showCopyFeedback(message) { |
||||
const btn = event.target; |
||||
const originalText = btn.textContent; |
||||
btn.textContent = message; |
||||
btn.style.backgroundColor = "#28a745"; |
||||
setTimeout(() => { |
||||
btn.textContent = originalText; |
||||
btn.style.backgroundColor = ""; |
||||
}, 2000); |
||||
} |
||||
|
||||
@ -0,0 +1,62 @@
|
||||
<!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> |
||||
<link rel="stylesheet" href="/static/style.css"> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<img class="logo" src="{{.LogoURI}}" alt="Dex"> |
||||
<form id="login-form" action="/login" method="post"> |
||||
<div class="app-description"> |
||||
This is an example application for <b>Dex</b> OpenID Connect provider.<br> |
||||
Learn more in the <a href="https://dexidp.io/docs/" target="_blank">documentation</a>. |
||||
</div> |
||||
<div class="primary-action"> |
||||
<input type="submit" value="Login"> |
||||
</div> |
||||
<details class="advanced"> |
||||
<summary>Advanced options</summary> |
||||
<div class="field"> |
||||
<label>Scopes:</label> |
||||
<small class="hint">Select OpenID Connect scopes to request. Standard scopes are pre-selected.</small> |
||||
<div class="scopes-list" id="scopes-list"> |
||||
{{range .ScopesSupported}} |
||||
<div class="scope-item"> |
||||
<input type="checkbox" name="extra_scopes" value="{{.}}" id="scope_{{.}}"> |
||||
<label for="scope_{{.}}">{{.}}</label> |
||||
</div> |
||||
{{end}} |
||||
</div> |
||||
{{if eq (len .ScopesSupported) 0}} |
||||
<div class="hint">No scopes from discovery - add custom scopes below.</div> |
||||
{{end}} |
||||
<div class="inline-input"> |
||||
<input type="text" id="custom_scope_input" placeholder="custom-scope"> |
||||
<button type="button" id="add-custom-scope">Add scope</button> |
||||
</div> |
||||
</div> |
||||
<div class="field"> |
||||
<label for="cross_client_input">Cross-Client auth:</label> |
||||
<small class="hint">Each client is sent as audience:server:client_id scope.</small> |
||||
<div id="cross-client-list"></div> |
||||
<div class="inline-input"> |
||||
<input type="text" id="cross_client_input" placeholder="client-id"> |
||||
<button type="button" id="add-cross-client">Add</button> |
||||
</div> |
||||
</div> |
||||
<div class="field"> |
||||
<label for="connector_id">Connector:</label> |
||||
<small class="hint">Specify a connector ID to bypass the connector selection screen.</small> |
||||
<input type="text" id="connector_id" name="connector_id" placeholder="connector id"> |
||||
</div> |
||||
</details> |
||||
</form> |
||||
</div> |
||||
|
||||
<script src="/static/app.js"></script> |
||||
</body> |
||||
</html> |
||||
|
||||
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Tokens</title> |
||||
<link rel="stylesheet" href="/static/style.css"> |
||||
</head> |
||||
<body class="token-page"> |
||||
{{ if .IDToken }} |
||||
<div class="token-block"> |
||||
<div class="token-title"> |
||||
ID Token: |
||||
<a href="{{.IDTokenJWTLink}}" target="_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="{{.AccessTokenJWTLink}}" target="_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 }} |
||||
|
||||
{{ if .PublicKeyPEM }} |
||||
<div class="token-block"> |
||||
<div class="token-title"> |
||||
Public Key (for JWT verification): |
||||
<button type="button" class="copy-btn" onclick="copyPublicKey()">Copy to Clipboard</button> |
||||
</div> |
||||
<pre><code id="public-key">{{ .PublicKeyPEM }}</code></pre> |
||||
<div class="hint">Copy this key and paste it into jwt.io's "Verify Signature" section to validate the token signature.</div> |
||||
</div> |
||||
{{ end }} |
||||
|
||||
<a href="/" class="back-button">Back to Home</a> |
||||
|
||||
<script src="/static/token.js"></script> |
||||
</body> |
||||
</html> |
||||
|
||||
Loading…
Reference in new issue