Skip to content

Commit 76b9b0b

Browse files
committed
fix: require login before loading admin surfaces
1 parent c69cbfc commit 76b9b0b

2 files changed

Lines changed: 168 additions & 5 deletions

File tree

internal/assets_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ func TestEmbeddedAdminShell(t *testing.T) {
1010
required := []string{
1111
`data-admin-shell-version`,
1212
`data-contributions-endpoint`,
13+
`data-login-endpoint`,
14+
`data-token-storage-key`,
15+
`id="login-form"`,
1316
`id="contribution-nav"`,
1417
`id="contribution-list"`,
18+
`Authorization`,
1519
}
1620
for _, needle := range required {
1721
if !strings.Contains(html, needle) {

internal/ui_dist/index.html

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,55 @@
176176
font-weight: 700;
177177
text-decoration: none;
178178
}
179+
.hidden { display: none !important; }
180+
.login-panel {
181+
max-width: 420px;
182+
}
183+
.field {
184+
display: grid;
185+
gap: 6px;
186+
margin-bottom: 12px;
187+
}
188+
.field label {
189+
color: var(--muted);
190+
font-size: 13px;
191+
font-weight: 700;
192+
}
193+
.field input {
194+
width: 100%;
195+
min-height: 40px;
196+
border: 1px solid var(--line);
197+
border-radius: 6px;
198+
padding: 8px 10px;
199+
color: var(--ink);
200+
font: inherit;
201+
background: #fff;
202+
}
203+
.primary-button,
204+
.secondary-button {
205+
min-height: 36px;
206+
border-radius: 6px;
207+
padding: 8px 12px;
208+
font: inherit;
209+
font-weight: 700;
210+
cursor: pointer;
211+
}
212+
.primary-button {
213+
border: 1px solid var(--accent);
214+
color: var(--accent-ink);
215+
background: var(--accent);
216+
}
217+
.secondary-button {
218+
border: 1px solid var(--line);
219+
color: var(--muted);
220+
background: var(--panel);
221+
}
222+
.button-row {
223+
display: flex;
224+
align-items: center;
225+
gap: 10px;
226+
flex-wrap: wrap;
227+
}
179228
@media (max-width: 860px) {
180229
.shell { grid-template-columns: 1fr; }
181230
aside { position: static; }
@@ -185,13 +234,13 @@
185234
</style>
186235
</head>
187236
<body>
188-
<div class="shell" data-admin-shell-version="1" data-contributions-endpoint="/api/admin/contributions">
237+
<div class="shell" data-admin-shell-version="1" data-contributions-endpoint="/api/admin/contributions" data-login-endpoint="/api/admin/auth/login" data-token-storage-key="workflow.admin.token">
189238
<aside>
190239
<div class="brand">
191240
<strong>Workflow Admin</strong>
192241
<span id="target-app-label">Application control plane</span>
193242
</div>
194-
<nav>
243+
<nav id="admin-nav">
195244
<div class="nav-section">Core</div>
196245
<button class="nav-item active" type="button" data-panel="overview-panel">Overview</button>
197246
<div class="nav-section" id="contribution-nav-heading">Surfaces</div>
@@ -201,10 +250,28 @@
201250
<main>
202251
<header>
203252
<h1 id="panel-title">Overview</h1>
204-
<div class="status" id="load-status">Loading contributions</div>
253+
<div class="status" id="load-status">Checking session</div>
205254
</header>
206255

207-
<section class="grid" aria-label="Admin status">
256+
<section id="login-panel" class="panel active login-panel">
257+
<h2>Sign In</h2>
258+
<form id="login-form" autocomplete="on">
259+
<div class="field">
260+
<label for="admin-email">Email</label>
261+
<input id="admin-email" name="email" type="email" autocomplete="username" required>
262+
</div>
263+
<div class="field">
264+
<label for="admin-password">Password</label>
265+
<input id="admin-password" name="password" type="password" autocomplete="current-password" required>
266+
</div>
267+
<div class="button-row">
268+
<button class="primary-button" type="submit">Sign in</button>
269+
<span id="login-error" class="warn hidden"></span>
270+
</div>
271+
</form>
272+
</section>
273+
274+
<section class="grid hidden" id="admin-status-grid" aria-label="Admin status">
208275
<div class="card"><span>Registered surfaces</span><strong id="surface-count">0</strong></div>
209276
<div class="card"><span>Categories</span><strong id="category-count">0</strong></div>
210277
<div class="card"><span>Active surface</span><strong id="active-surface">Overview</strong></div>
@@ -226,13 +293,49 @@ <h2>Contributions</h2>
226293
const shell = document.querySelector('.shell');
227294
const status = document.getElementById('load-status');
228295
const title = document.getElementById('panel-title');
296+
const adminNav = document.getElementById('admin-nav');
229297
const nav = document.getElementById('contribution-nav');
230298
const navHeading = document.getElementById('contribution-nav-heading');
231299
const list = document.getElementById('contribution-list');
232300
const overviewContent = document.getElementById('overview-content');
301+
const loginForm = document.getElementById('login-form');
302+
const loginError = document.getElementById('login-error');
303+
const statusGrid = document.getElementById('admin-status-grid');
233304
const surfaceCount = document.getElementById('surface-count');
234305
const categoryCount = document.getElementById('category-count');
235306
const activeSurface = document.getElementById('active-surface');
307+
const tokenStorageKey = shell.dataset.tokenStorageKey || 'workflow.admin.token';
308+
309+
function storedToken() {
310+
try {
311+
return window.localStorage.getItem(tokenStorageKey) || '';
312+
} catch (_) {
313+
return '';
314+
}
315+
}
316+
317+
function storeToken(token) {
318+
try {
319+
window.localStorage.setItem(tokenStorageKey, token);
320+
} catch (_) {
321+
// Storage can be unavailable in private or embedded contexts.
322+
}
323+
}
324+
325+
function clearToken() {
326+
try {
327+
window.localStorage.removeItem(tokenStorageKey);
328+
} catch (_) {
329+
// Storage can be unavailable in private or embedded contexts.
330+
}
331+
}
332+
333+
function authHeaders(extra = {}) {
334+
const token = storedToken();
335+
const headers = { ...extra };
336+
if (token) headers.Authorization = `Bearer ${token}`;
337+
return headers;
338+
}
236339

237340
function showPanel(id, label) {
238341
document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id === id));
@@ -241,6 +344,27 @@ <h2>Contributions</h2>
241344
activeSurface.textContent = label;
242345
}
243346

347+
function showLogin(message = '') {
348+
clearToken();
349+
statusGrid.classList.add('hidden');
350+
adminNav.classList.add('hidden');
351+
nav.textContent = '';
352+
navHeading.hidden = true;
353+
document.querySelectorAll('.panel').forEach(panel => panel.classList.remove('active'));
354+
document.getElementById('login-panel').classList.add('active');
355+
title.textContent = 'Sign In';
356+
status.textContent = 'Sign in required';
357+
loginError.textContent = message;
358+
loginError.classList.toggle('hidden', !message);
359+
}
360+
361+
function showAdminShell() {
362+
statusGrid.classList.remove('hidden');
363+
adminNav.classList.remove('hidden');
364+
document.getElementById('login-panel').classList.remove('active');
365+
showPanel('overview-panel', 'Overview');
366+
}
367+
244368
document.querySelectorAll('.nav-item').forEach(item => {
245369
item.addEventListener('click', () => showPanel(item.dataset.panel, item.textContent.trim()));
246370
});
@@ -335,8 +459,17 @@ <h2>Contributions</h2>
335459
}
336460

337461
async function loadContributions() {
462+
if (!storedToken()) {
463+
showLogin();
464+
return;
465+
}
466+
showAdminShell();
338467
try {
339-
const response = await fetch(shell.dataset.contributionsEndpoint, { headers: { accept: 'application/json' } });
468+
const response = await fetch(shell.dataset.contributionsEndpoint, { headers: authHeaders({ accept: 'application/json' }) });
469+
if (response.status === 401 || response.status === 403) {
470+
showLogin('Your session is not authorized for this admin.');
471+
return;
472+
}
340473
if (!response.ok) throw new Error(`HTTP ${response.status}`);
341474
const payload = await response.json();
342475
renderContributions(payload.contributions || []);
@@ -347,6 +480,32 @@ <h2>Contributions</h2>
347480
}
348481
}
349482

483+
loginForm.addEventListener('submit', async event => {
484+
event.preventDefault();
485+
loginError.classList.add('hidden');
486+
status.textContent = 'Signing in';
487+
const form = new FormData(loginForm);
488+
try {
489+
const response = await fetch(shell.dataset.loginEndpoint, {
490+
method: 'POST',
491+
headers: { 'content-type': 'application/json', accept: 'application/json' },
492+
body: JSON.stringify({
493+
email: String(form.get('email') || ''),
494+
password: String(form.get('password') || '')
495+
})
496+
});
497+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
498+
const payload = await response.json();
499+
const token = payload.token || payload.access_token;
500+
if (!token) throw new Error('missing token');
501+
storeToken(token);
502+
loginForm.reset();
503+
await loadContributions();
504+
} catch (error) {
505+
showLogin('Sign-in failed.');
506+
}
507+
});
508+
350509
loadContributions();
351510
</script>
352511
</body>

0 commit comments

Comments
 (0)