Skip to content

Commit 2a82da5

Browse files
committed
feat: auth UI improvements — show/hide toggle and status dot
- Eye icon to toggle token visibility (password/text) - Green/red/yellow status dot with auto-verify on save and page load - Placeholder hint when token is already saved - Clears input after save for security
1 parent b066ace commit 2a82da5

1 file changed

Lines changed: 139 additions & 9 deletions

File tree

web2api/templates/index.html

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,72 @@
126126
margin-top: 0.5rem;
127127
}
128128

129+
.auth-input-wrap {
130+
flex: 1 1 320px;
131+
position: relative;
132+
display: flex;
133+
align-items: center;
134+
}
135+
136+
.auth-input-wrap .filter {
137+
width: 100%;
138+
padding-right: 2.6rem;
139+
}
140+
141+
.auth-toggle {
142+
position: absolute;
143+
right: 0.55rem;
144+
background: none;
145+
border: none;
146+
cursor: pointer;
147+
color: var(--ink-dim);
148+
font-size: 1.1rem;
149+
padding: 0.2rem;
150+
line-height: 1;
151+
opacity: 0.6;
152+
transition: opacity 150ms;
153+
}
154+
155+
.auth-toggle:hover {
156+
opacity: 1;
157+
}
158+
159+
.auth-status-dot {
160+
display: inline-flex;
161+
align-items: center;
162+
gap: 0.35rem;
163+
font-size: 0.82rem;
164+
color: var(--ink-dim);
165+
}
166+
167+
.auth-status-dot::before {
168+
content: "";
169+
display: inline-block;
170+
width: 8px;
171+
height: 8px;
172+
border-radius: 50%;
173+
background: #94a3b8;
174+
flex-shrink: 0;
175+
}
176+
177+
.auth-status-dot.is-valid::before {
178+
background: #22c55e;
179+
}
180+
181+
.auth-status-dot.is-invalid::before {
182+
background: #ef4444;
183+
}
184+
185+
.auth-status-dot.is-checking::before {
186+
background: #f59e0b;
187+
animation: pulse-dot 1s ease infinite;
188+
}
189+
190+
@keyframes pulse-dot {
191+
0%, 100% { opacity: 1; }
192+
50% { opacity: 0.4; }
193+
}
194+
129195
.view-tabs {
130196
margin-top: 0.9rem;
131197
display: inline-flex;
@@ -630,16 +696,20 @@ <h1>Web2API Recipes</h1>
630696
</div>
631697
</div>
632698
<div class="auth-row">
633-
<input
634-
class="filter auth-input"
635-
id="authTokenInput"
636-
type="password"
637-
placeholder="Paste access token"
638-
autocomplete="current-password"
639-
spellcheck="false"
640-
/>
699+
<div class="auth-input-wrap">
700+
<input
701+
class="filter"
702+
id="authTokenInput"
703+
type="password"
704+
placeholder="Paste access token"
705+
autocomplete="current-password"
706+
spellcheck="false"
707+
/>
708+
<button class="auth-toggle" id="authToggleVis" type="button" title="Show/hide token">👁</button>
709+
</div>
641710
<button class="manager-btn" id="authSave" type="button">Save</button>
642711
<button class="manager-btn" id="authClear" type="button">Clear</button>
712+
<span class="auth-status-dot" id="authDot"></span>
643713
</div>
644714
<p class="manager-meta auth-meta">
645715
Send <code>{{ auth.header }}: {{ auth.scheme }} &lt;token&gt;</code> or
@@ -1614,8 +1684,63 @@ <h2>MCP Tool Bridge</h2>
16141684
mcpReload.addEventListener("click", () => void loadMcpTools());
16151685
}
16161686

1687+
// Show/hide token toggle
1688+
const authToggle = document.getElementById("authToggleVis");
1689+
if (authToggle && authTokenInput) {
1690+
authToggle.addEventListener("click", () => {
1691+
const isPassword = authTokenInput.type === "password";
1692+
authTokenInput.type = isPassword ? "text" : "password";
1693+
authToggle.textContent = isPassword ? "🙈" : "👁";
1694+
authToggle.title = isPassword ? "Hide token" : "Show token";
1695+
});
1696+
}
1697+
1698+
// Auth status dot
1699+
const authDot = document.getElementById("authDot");
1700+
function setAuthDot(state, label) {
1701+
if (!authDot) return;
1702+
authDot.className = "auth-status-dot";
1703+
if (state) authDot.classList.add(state);
1704+
authDot.textContent = label || "";
1705+
}
1706+
1707+
async function verifyToken() {
1708+
const token = getStoredToken().trim();
1709+
if (!token) {
1710+
setAuthDot("", "No token");
1711+
return;
1712+
}
1713+
setAuthDot("is-checking", "Verifying…");
1714+
try {
1715+
const resp = await fetch("/api/recipes/manage", {
1716+
headers: { "Authorization": "Bearer " + token },
1717+
});
1718+
if (resp.ok) {
1719+
setAuthDot("is-valid", "Token valid");
1720+
} else if (resp.status === 401 || resp.status === 403) {
1721+
setAuthDot("is-invalid", "Token rejected");
1722+
} else {
1723+
setAuthDot("is-invalid", "Error " + resp.status);
1724+
}
1725+
} catch {
1726+
setAuthDot("is-invalid", "Connection error");
1727+
}
1728+
}
1729+
1730+
// Verify on page load if token exists
1731+
if (getStoredToken().trim()) {
1732+
void verifyToken();
1733+
} else {
1734+
setAuthDot("", "No token");
1735+
}
1736+
1737+
// Pre-fill input hint if token is stored
1738+
if (authTokenInput && getStoredToken().trim()) {
1739+
authTokenInput.placeholder = "•••••••• (token saved — enter new to replace)";
1740+
}
1741+
16171742
if (authSave) {
1618-
authSave.addEventListener("click", () => {
1743+
authSave.addEventListener("click", async () => {
16191744
if (!authTokenInput) {
16201745
return;
16211746
}
@@ -1625,7 +1750,10 @@ <h2>MCP Tool Bridge</h2>
16251750
return;
16261751
}
16271752
storeToken(value);
1753+
authTokenInput.value = "";
1754+
authTokenInput.placeholder = "•••••••• (token saved — enter new to replace)";
16281755
setAuthStatus("Access token saved in this browser.");
1756+
await verifyToken();
16291757
void loadCatalog();
16301758
if (mcpLoaded) {
16311759
void loadMcpTools();
@@ -1638,7 +1766,9 @@ <h2>MCP Tool Bridge</h2>
16381766
clearStoredToken();
16391767
if (authTokenInput) {
16401768
authTokenInput.value = "";
1769+
authTokenInput.placeholder = "Paste access token";
16411770
}
1771+
setAuthDot("", "No token");
16421772
clearProtectedViews();
16431773
setStatus("Access token cleared. Repository access is disabled until you enter it again.", {
16441774
error: true,

0 commit comments

Comments
 (0)