229 lines
7.2 KiB
TypeScript
229 lines
7.2 KiB
TypeScript
/**
|
|
* Token Status UI - Standalone
|
|
* Displays token statuses, expiration times, last 4 digits
|
|
*/
|
|
|
|
class TokenStatusUI {
|
|
private container: HTMLElement | null = null;
|
|
private refreshInterval: number | null = null;
|
|
|
|
/**
|
|
* Create and show token status UI
|
|
*/
|
|
create(containerId: string = 'app'): void {
|
|
this.container = document.getElementById(containerId);
|
|
if (!this.container) {
|
|
console.error('[TokenUI] Container not found:', containerId);
|
|
return;
|
|
}
|
|
|
|
this.render();
|
|
|
|
// Refresh UI every second to update expiration countdown
|
|
this.refreshInterval = window.setInterval(() => {
|
|
this.render();
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Render token status UI
|
|
*/
|
|
private render(): void {
|
|
if (!this.container) return;
|
|
|
|
const state = this.getState();
|
|
|
|
const pbStatus = state.pbUser.active ? '✓ Active' : '✗ Inactive';
|
|
const pbStatusColor = state.pbUser.active ? '#10b981' : '#ef4444';
|
|
|
|
const graphStatus = state.graphUser.active ? '✓ Active' : '○ Not Required';
|
|
const graphStatusColor = state.graphUser.active ? '#10b981' : '#6b7280';
|
|
|
|
this.container.innerHTML = `
|
|
<div style="
|
|
min-h-screen;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
">
|
|
<div style="
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
padding: 40px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
">
|
|
<!-- Header -->
|
|
<div style="margin-bottom: 30px;">
|
|
<h1 style="font-size: 28px; font-weight: bold; color: #1f2937; margin: 0 0 10px 0;">
|
|
Job Info Auth
|
|
</h1>
|
|
<p style="color: #6b7280; margin: 0; font-size: 14px;">
|
|
User: <strong>${state.userDisplayName}</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- PocketBase User Token -->
|
|
<div style="
|
|
margin-bottom: 24px;
|
|
padding: 20px;
|
|
background: #f9fafb;
|
|
border-radius: 8px;
|
|
border-left: 4px solid ${pbStatusColor};
|
|
">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
<h2 style="font-size: 14px; font-weight: 600; color: #1f2937; margin: 0;">
|
|
PocketBase User Token
|
|
</h2>
|
|
<span style="color: ${pbStatusColor}; font-weight: 600; font-size: 13px;">
|
|
${pbStatus}
|
|
</span>
|
|
</div>
|
|
<p style="color: #6b7280; font-size: 12px; margin: 8px 0; font-family: monospace;">
|
|
Token: ...${state.pbUser.lastFour}
|
|
</p>
|
|
<p style="color: #6b7280; font-size: 12px; margin: 8px 0;">
|
|
Expires in: <strong style="color: #1f2937;">${state.pbUser.expiresIn}</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Graph User Token -->
|
|
<div style="
|
|
margin-bottom: 24px;
|
|
padding: 20px;
|
|
background: #f9fafb;
|
|
border-radius: 8px;
|
|
border-left: 4px solid ${graphStatusColor};
|
|
">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
<h2 style="font-size: 14px; font-weight: 600; color: #1f2937; margin: 0;">
|
|
Microsoft Graph Token
|
|
</h2>
|
|
<span style="color: ${graphStatusColor}; font-weight: 600; font-size: 13px;">
|
|
${graphStatus}
|
|
</span>
|
|
</div>
|
|
${state.graphUser.active ? `
|
|
<p style="color: #6b7280; font-size: 12px; margin: 8px 0; font-family: monospace;">
|
|
Token: ...${state.graphUser.lastFour}
|
|
</p>
|
|
<p style="color: #6b7280; font-size: 12px; margin: 8px 0;">
|
|
Expires in: <strong style="color: #1f2937;">${state.graphUser.expiresIn}</strong>
|
|
</p>
|
|
` : `
|
|
<p style="color: #6b7280; font-size: 12px; margin: 8px 0;">
|
|
Graph token is optional. Login proceeded without it.
|
|
</p>
|
|
`}
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div style="display: flex; gap: 10px;">
|
|
<button onclick="window.JobInfoAuth.logout(); location.reload();" style="
|
|
flex: 1;
|
|
padding: 12px;
|
|
background: #ef4444;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
">
|
|
Logout
|
|
</button>
|
|
<button onclick="location.reload();" style="
|
|
flex: 1;
|
|
padding: 12px;
|
|
background: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
">
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Debug Info -->
|
|
<div style="
|
|
margin-top: 30px;
|
|
padding: 15px;
|
|
background: #f0f9ff;
|
|
border-radius: 6px;
|
|
border: 1px solid #bfdbfe;
|
|
font-size: 11px;
|
|
color: #1e40af;
|
|
font-family: monospace;
|
|
word-break: break-all;
|
|
">
|
|
<strong>Debug Info:</strong><br>
|
|
pb-user: ${state.pbUser.active ? 'active' : 'inactive'} | graph-user: ${state.graphUser.active ? 'active' : 'inactive'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Get current token state
|
|
*/
|
|
private getState() {
|
|
const auth = window.JobInfoAuth;
|
|
const authState = auth.getState();
|
|
|
|
const calculateTimeLeft = (expiresAt) => {
|
|
if (!expiresAt) return 'unknown';
|
|
const now = Date.now();
|
|
const diff = expiresAt - now;
|
|
if (diff < 0) return 'expired';
|
|
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
return `${minutes}m`;
|
|
};
|
|
|
|
return {
|
|
pbUser: {
|
|
active: !!authState['pb-user'],
|
|
lastFour: authState['pb-user']?.lastFourDigits || 'none',
|
|
expiresIn: calculateTimeLeft(authState['pb-user']?.expiresAt),
|
|
expiresAt: authState['pb-user']?.expiresAt
|
|
},
|
|
graphUser: {
|
|
active: !!authState['graph-user'],
|
|
lastFour: authState['graph-user']?.lastFourDigits || 'none',
|
|
expiresIn: calculateTimeLeft(authState['graph-user']?.expiresAt),
|
|
expiresAt: authState['graph-user']?.expiresAt
|
|
},
|
|
userDisplayName: authState.userDisplayName || 'Unknown'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Destroy UI and cleanup
|
|
*/
|
|
destroy(): void {
|
|
if (this.refreshInterval) {
|
|
clearInterval(this.refreshInterval);
|
|
}
|
|
}
|
|
}
|
|
|
|
const TokenUI = new TokenStatusUI();
|
|
|
|
// Expose globally
|
|
if (typeof window !== 'undefined') {
|
|
window.TokenUI = TokenUI;
|
|
}
|
|
|
|
export default TokenUI;
|