fixed login loop
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Job-Info Auth Flow - Standalone
|
||||
* Complete auth lifecycle: acquire tokens, refresh, persist, retrieve
|
||||
* No external imports - loads PocketBase from global scope
|
||||
*/
|
||||
|
||||
class JobInfoAuthFlow {
|
||||
constructor() {
|
||||
this.pb = null;
|
||||
this.tokens = {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize: load persisted tokens, check if active, prompt login if needed
|
||||
*/
|
||||
async init() {
|
||||
console.log('[JobInfoAuth] init() called');
|
||||
|
||||
// PocketBase is loaded via script tag
|
||||
if (typeof PocketBase === 'undefined') {
|
||||
throw new Error('PocketBase SDK not loaded. Check script tag in HTML.');
|
||||
}
|
||||
|
||||
this.pb = new PocketBase('http://localhost:8090');
|
||||
console.log('[JobInfoAuth] PocketBase initialized:', this.pb);
|
||||
|
||||
// Load tokens from localStorage
|
||||
this.loadTokens();
|
||||
console.log('[JobInfoAuth] Tokens loaded from storage:', Object.keys(this.tokens));
|
||||
|
||||
// Check if tokens are active
|
||||
const hasActiveTokens = this.isTokenActive('pb-user') || this.isTokenActive('graph-user');
|
||||
|
||||
if (!hasActiveTokens) {
|
||||
console.log('[JobInfoAuth] No active tokens, prompting login...');
|
||||
await this.promptLogin();
|
||||
} else {
|
||||
console.log('[JobInfoAuth] Tokens active, refreshing...');
|
||||
await this.refreshAllTokens();
|
||||
}
|
||||
|
||||
console.log('[JobInfoAuth] init() complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user to log in via OAuth popup
|
||||
*/
|
||||
async promptLogin() {
|
||||
console.log('[JobInfoAuth] promptLogin() called');
|
||||
|
||||
try {
|
||||
const authData = await this.pb.collection('users').authWithOAuth2({
|
||||
provider: 'microsoft'
|
||||
});
|
||||
|
||||
console.log('[JobInfoAuth] OAuth successful, authData:', authData);
|
||||
|
||||
// Store PocketBase user token
|
||||
const pbToken = authData.token;
|
||||
const displayName = authData.record?.name || authData.record?.email || 'Unknown';
|
||||
|
||||
this.storeToken('pb-user', pbToken, displayName);
|
||||
console.log('[JobInfoAuth] pb-user token stored');
|
||||
|
||||
// Extract and store Graph token from OAuth metadata
|
||||
const graphToken = this.extractGraphToken(authData.meta || {});
|
||||
if (graphToken) {
|
||||
this.storeToken('graph-user', graphToken, displayName);
|
||||
console.log('[JobInfoAuth] graph-user token stored');
|
||||
} else {
|
||||
console.log('[JobInfoAuth] No graph token in OAuth response');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('[JobInfoAuth] OAuth failed:', err);
|
||||
throw new Error(`Login failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Graph token from OAuth metadata
|
||||
*/
|
||||
extractGraphToken(metadata) {
|
||||
console.log('[JobInfoAuth] extractGraphToken() called, metadata:', metadata);
|
||||
|
||||
if (!metadata) return null;
|
||||
|
||||
// Microsoft OAuth response includes access_token for Graph
|
||||
if (metadata.accessToken) {
|
||||
return metadata.accessToken;
|
||||
}
|
||||
if (metadata.access_token) {
|
||||
return metadata.access_token;
|
||||
}
|
||||
|
||||
console.log('[JobInfoAuth] No Graph token found in metadata');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all tokens - keep them fresh, only replace pb-user on new token
|
||||
*/
|
||||
async refreshAllTokens() {
|
||||
console.log('[JobInfoAuth] refreshAllTokens() called');
|
||||
|
||||
try {
|
||||
// Refresh pb-user token
|
||||
if (this.tokens['pb-user']) {
|
||||
const oldToken = this.tokens['pb-user'].value;
|
||||
|
||||
// Use PocketBase refresh
|
||||
const authData = await this.pb.collection('users').authRefresh();
|
||||
const newToken = authData.token;
|
||||
|
||||
// Only replace if genuinely new
|
||||
if (newToken && newToken !== oldToken) {
|
||||
console.log('[JobInfoAuth] pb-user token refreshed (new token)');
|
||||
const displayName = this.tokens['pb-user'].displayName;
|
||||
this.storeToken('pb-user', newToken, displayName);
|
||||
} else {
|
||||
console.log('[JobInfoAuth] pb-user token still valid, not replacing');
|
||||
}
|
||||
}
|
||||
|
||||
// Graph token: don't require for refresh, but use if available
|
||||
if (this.tokens['graph-user']) {
|
||||
console.log('[JobInfoAuth] graph-user token exists, keeping it');
|
||||
// Optionally refresh graph token here if needed
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.warn('[JobInfoAuth] Token refresh had issues:', err.message);
|
||||
// Don't throw - tokens might still be valid
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store token with metadata
|
||||
*/
|
||||
storeToken(type, token, displayName) {
|
||||
console.log('[JobInfoAuth] storeToken():', type);
|
||||
|
||||
const lastFourDigits = token.slice(-4);
|
||||
const decoded = this.decodeToken(token);
|
||||
const expiresAt = decoded?.exp ? decoded.exp * 1000 : Date.now() + (3600 * 1000); // Default 1 hour
|
||||
|
||||
this.tokens[type] = {
|
||||
value: token,
|
||||
displayName,
|
||||
lastFourDigits,
|
||||
expiresAt,
|
||||
storedAt: Date.now()
|
||||
};
|
||||
|
||||
// Persist to localStorage
|
||||
localStorage.setItem(`job-info-${type}`, JSON.stringify(this.tokens[type]));
|
||||
console.log(`[JobInfoAuth] ${type} stored, expires at:`, new Date(expiresAt).toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tokens from localStorage
|
||||
*/
|
||||
loadTokens() {
|
||||
console.log('[JobInfoAuth] loadTokens() called');
|
||||
|
||||
const types = ['pb-user', 'graph-user', 'pb-agent', 'graph-agent'];
|
||||
|
||||
types.forEach(type => {
|
||||
const stored = localStorage.getItem(`job-info-${type}`);
|
||||
if (stored) {
|
||||
try {
|
||||
this.tokens[type] = JSON.parse(stored);
|
||||
console.log(`[JobInfoAuth] Loaded ${type} from localStorage`);
|
||||
} catch (err) {
|
||||
console.warn(`[JobInfoAuth] Failed to parse ${type}:`, err);
|
||||
localStorage.removeItem(`job-info-${type}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is active (exists and not expired)
|
||||
*/
|
||||
isTokenActive(type) {
|
||||
const token = this.tokens[type];
|
||||
if (!token) {
|
||||
console.log(`[JobInfoAuth] isTokenActive(${type}): no token stored`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isExpired = token.expiresAt && Date.now() > token.expiresAt;
|
||||
if (isExpired) {
|
||||
console.log(`[JobInfoAuth] isTokenActive(${type}): expired`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[JobInfoAuth] isTokenActive(${type}): active`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current auth state
|
||||
*/
|
||||
getState() {
|
||||
return {
|
||||
'pb-user': this.tokens['pb-user'] || null,
|
||||
'graph-user': this.tokens['graph-user'] || null,
|
||||
'pb-agent': this.tokens['pb-agent'] || null,
|
||||
'graph-agent': this.tokens['graph-agent'] || null,
|
||||
userDisplayName: this.tokens['pb-user']?.displayName || 'Unknown'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout - clear all tokens
|
||||
*/
|
||||
logout() {
|
||||
console.log('[JobInfoAuth] logout() called');
|
||||
|
||||
const types = ['pb-user', 'graph-user', 'pb-agent', 'graph-agent'];
|
||||
types.forEach(type => {
|
||||
localStorage.removeItem(`job-info-${type}`);
|
||||
});
|
||||
|
||||
this.tokens = {};
|
||||
|
||||
if (this.pb) {
|
||||
this.pb.authStore.clear();
|
||||
}
|
||||
|
||||
console.log('[JobInfoAuth] All tokens cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT token to get exp claim
|
||||
*/
|
||||
decodeToken(token) {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const decoded = JSON.parse(atob(parts[1]));
|
||||
return decoded;
|
||||
} catch (err) {
|
||||
console.warn('[JobInfoAuth] Failed to decode token:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const JobInfoAuth = new JobInfoAuthFlow();
|
||||
|
||||
// Expose globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.JobInfoAuth = JobInfoAuth;
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Job-Info Auth Flow - Standalone
|
||||
*
|
||||
* Complete, self-contained authentication for Job-Info app
|
||||
* - Check localStorage for active tokens on load
|
||||
* - Prompt for login if tokens missing/inactive
|
||||
* - Get user display name from PocketBase metadata
|
||||
* - Refresh all tokens to keep them fresh
|
||||
* - Only replace pb-user when we have a new token
|
||||
* - Graph token is optional after first login
|
||||
* - Do not prompt again unless pb-user is missing/inactive
|
||||
*/
|
||||
|
||||
interface TokenMetadata {
|
||||
value: string;
|
||||
displayName: string;
|
||||
lastFourDigits: string;
|
||||
expiresAt: number;
|
||||
acquiredAt: number;
|
||||
}
|
||||
|
||||
interface JobInfoAuthState {
|
||||
'pb-user'?: TokenMetadata;
|
||||
'graph-user'?: TokenMetadata;
|
||||
userDisplayName: string;
|
||||
}
|
||||
|
||||
class JobInfoAuthFlow {
|
||||
private state: JobInfoAuthState = {
|
||||
userDisplayName: 'Unknown'
|
||||
};
|
||||
private pb: any = null;
|
||||
private initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize and check token status
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
console.log('[JobInfoAuth] Starting init');
|
||||
|
||||
// Initialize PocketBase
|
||||
const PocketBase = (window as any).PocketBase;
|
||||
if (!PocketBase) {
|
||||
throw new Error('PocketBase SDK not loaded. Include: <script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>');
|
||||
}
|
||||
|
||||
this.pb = new PocketBase('http://localhost:8090');
|
||||
console.log('[JobInfoAuth] PocketBase initialized');
|
||||
|
||||
// Load persisted tokens from localStorage
|
||||
this.loadPersistedTokens();
|
||||
|
||||
// Check token status
|
||||
const pbUserActive = this.isTokenActive('pb-user');
|
||||
const graphUserActive = this.isTokenActive('graph-user');
|
||||
|
||||
console.log('[JobInfoAuth] Token status - pb-user:', pbUserActive, 'graph-user:', graphUserActive);
|
||||
|
||||
// If pb-user is missing or inactive: prompt for login
|
||||
if (!pbUserActive) {
|
||||
console.log('[JobInfoAuth] pb-user token missing or inactive, prompting login');
|
||||
await this.promptLogin();
|
||||
} else {
|
||||
console.log('[JobInfoAuth] pb-user token is active, loading display name');
|
||||
// Get display name from state or try to refresh
|
||||
if (!this.state.userDisplayName || this.state.userDisplayName === 'Unknown') {
|
||||
const userInfo = this.getUserInfo();
|
||||
this.state.userDisplayName = userInfo.displayName || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh all tokens to keep them fresh
|
||||
await this.refreshAllTokens();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[JobInfoAuth] Initialized. State:', this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for login via PocketBase OAuth
|
||||
*/
|
||||
private async promptLogin(): Promise<void> {
|
||||
console.log('[JobInfoAuth] Prompting for login with OAuth');
|
||||
|
||||
try {
|
||||
// Authenticate with PocketBase OAuth (Microsoft provider)
|
||||
const authData = await this.pb.collection('users').authWithOAuth2({
|
||||
provider: 'microsoft'
|
||||
});
|
||||
|
||||
console.log('[JobInfoAuth] OAuth successful, authData:', authData);
|
||||
|
||||
// Extract tokens
|
||||
const pbUserToken = authData.token;
|
||||
const graphUserToken = this.extractGraphToken(authData.meta || {});
|
||||
|
||||
if (!pbUserToken) {
|
||||
throw new Error('No pb-user token in OAuth response');
|
||||
}
|
||||
|
||||
// Get user display name
|
||||
const userInfo = this.getUserInfo();
|
||||
this.state.userDisplayName = userInfo.displayName || 'Unknown';
|
||||
|
||||
// Store tokens
|
||||
this.storeToken('pb-user', pbUserToken);
|
||||
if (graphUserToken) {
|
||||
this.storeToken('graph-user', graphUserToken);
|
||||
}
|
||||
|
||||
console.log('[JobInfoAuth] Login successful. User:', this.state.userDisplayName);
|
||||
} catch (err) {
|
||||
console.error('[JobInfoAuth] Login failed:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Graph token from OAuth metadata
|
||||
*/
|
||||
private extractGraphToken(meta: any): string | null {
|
||||
return (
|
||||
meta?.graphAccessToken ||
|
||||
meta?.graph_token ||
|
||||
meta?.graphToken ||
|
||||
meta?.accessToken ||
|
||||
meta?.access_token ||
|
||||
meta?.token ||
|
||||
meta?.rawToken ||
|
||||
meta?.authData?.access_token ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all tokens to keep them fresh
|
||||
*/
|
||||
private async refreshAllTokens(): Promise<void> {
|
||||
console.log('[JobInfoAuth] Refreshing all tokens');
|
||||
|
||||
try {
|
||||
// Refresh pb-user if we have an active session
|
||||
if (this.pb.authStore.isValid) {
|
||||
console.log('[JobInfoAuth] PocketBase session is valid, refreshing');
|
||||
const authData = await this.pb.collection('users').authRefresh();
|
||||
|
||||
const newPbToken = authData.token;
|
||||
const currentPbToken = this.state['pb-user']?.value;
|
||||
|
||||
// Only replace pb-user if we got a new token
|
||||
if (newPbToken && newPbToken !== currentPbToken) {
|
||||
console.log('[JobInfoAuth] pb-user token refreshed (new token)');
|
||||
this.storeToken('pb-user', newPbToken);
|
||||
} else if (newPbToken) {
|
||||
console.log('[JobInfoAuth] pb-user token still fresh, keeping existing');
|
||||
}
|
||||
|
||||
// Try to get updated graph token
|
||||
const graphToken = this.extractGraphToken(authData.meta || {});
|
||||
if (graphToken && graphToken !== this.state['graph-user']?.value) {
|
||||
console.log('[JobInfoAuth] graph-user token refreshed');
|
||||
this.storeToken('graph-user', graphToken);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[JobInfoAuth] Token refresh error:', err);
|
||||
// Don't throw - token refresh is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store token with metadata
|
||||
*/
|
||||
private storeToken(type: 'pb-user' | 'graph-user', token: string): void {
|
||||
const lastFour = token.substring(token.length - 4);
|
||||
const metadata: TokenMetadata = {
|
||||
value: token,
|
||||
displayName: this.state.userDisplayName,
|
||||
lastFourDigits: lastFour,
|
||||
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // Assume 7 days
|
||||
acquiredAt: Date.now()
|
||||
};
|
||||
|
||||
this.state[type] = metadata;
|
||||
|
||||
// Persist to localStorage
|
||||
const key = `auth:${type}`;
|
||||
localStorage.setItem(key, JSON.stringify(metadata));
|
||||
console.log(`[JobInfoAuth] Token stored: ${type} (last 4: ${lastFour})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tokens from localStorage
|
||||
*/
|
||||
private loadPersistedTokens(): void {
|
||||
try {
|
||||
const pbKey = 'auth:pb-user';
|
||||
const graphKey = 'auth:graph-user';
|
||||
|
||||
const pbData = localStorage.getItem(pbKey);
|
||||
const graphData = localStorage.getItem(graphKey);
|
||||
|
||||
if (pbData) {
|
||||
this.state['pb-user'] = JSON.parse(pbData);
|
||||
this.state.userDisplayName = this.state['pb-user'].displayName || 'Unknown';
|
||||
console.log('[JobInfoAuth] Loaded pb-user from localStorage');
|
||||
}
|
||||
|
||||
if (graphData) {
|
||||
this.state['graph-user'] = JSON.parse(graphData);
|
||||
console.log('[JobInfoAuth] Loaded graph-user from localStorage');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[JobInfoAuth] Error loading persisted tokens:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is active (exists and not expired)
|
||||
*/
|
||||
private isTokenActive(type: 'pb-user' | 'graph-user'): boolean {
|
||||
const token = this.state[type];
|
||||
if (!token || !token.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (token.expiresAt && token.expiresAt < Date.now()) {
|
||||
console.log(`[JobInfoAuth] Token expired: ${type}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info from PocketBase
|
||||
*/
|
||||
private getUserInfo(): { displayName: string; email: string } {
|
||||
const model = this.pb?.authStore?.model;
|
||||
if (!model) {
|
||||
return { displayName: 'Unknown', email: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
displayName: model.display_name || model.name || model.email || model.username || 'Unknown',
|
||||
email: model.email || model.username || ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current state (for UI)
|
||||
*/
|
||||
getState(): JobInfoAuthState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific token value
|
||||
*/
|
||||
getToken(type: 'pb-user' | 'graph-user'): string | undefined {
|
||||
return this.state[type]?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user display name
|
||||
*/
|
||||
getUserDisplayName(): string {
|
||||
return this.state.userDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout(): void {
|
||||
console.log('[JobInfoAuth] Logging out');
|
||||
this.pb?.authStore?.clear?.();
|
||||
localStorage.removeItem('auth:pb-user');
|
||||
localStorage.removeItem('auth:graph-user');
|
||||
this.state = { userDisplayName: 'Unknown' };
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
const JobInfoAuth = new JobInfoAuthFlow();
|
||||
|
||||
// Expose globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.JobInfoAuth = JobInfoAuth;
|
||||
}
|
||||
|
||||
export default JobInfoAuth;
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Token Status UI - Standalone
|
||||
* Displays token statuses, expiration times, last 4 digits
|
||||
* No external imports - uses window.JobInfoAuth from global scope
|
||||
*/
|
||||
|
||||
class TokenStatusUI {
|
||||
constructor() {
|
||||
this.container = null;
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show token status UI
|
||||
*/
|
||||
create(containerId = 'app') {
|
||||
console.log('[TokenUI] create() called with container:', containerId);
|
||||
|
||||
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);
|
||||
|
||||
console.log('[TokenUI] UI created and refresh interval started');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render token status UI
|
||||
*/
|
||||
render() {
|
||||
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-height: 100vh;
|
||||
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
|
||||
*/
|
||||
getState() {
|
||||
if (!window.JobInfoAuth) {
|
||||
return {
|
||||
pbUser: { active: false, lastFour: 'none', expiresIn: 'unknown' },
|
||||
graphUser: { active: false, lastFour: 'none', expiresIn: 'unknown' },
|
||||
userDisplayName: 'Unknown'
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const TokenUI = new TokenStatusUI();
|
||||
|
||||
// Expose globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.TokenUI = TokenUI;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user