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; }
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 >
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