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,668 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Job-Info OAuth Popup - Production Test</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e5e7eb;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.token-display {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.token-display.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.token-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.token-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.token-value {
|
||||
background: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
color: #1f2937;
|
||||
border: 1px solid #d1d5db;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-message.info {
|
||||
display: block;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
display: block;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #86efac;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
display: block;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #f9fafb;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e5e7eb;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.config-info {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.config-info strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin: 4px 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Job-Info OAuth Popup</h1>
|
||||
<p class="subtitle">Production OAuth login with Microsoft & PocketBase</p>
|
||||
|
||||
<!-- Configuration Display -->
|
||||
<div class="section">
|
||||
<h2>Active Configuration</h2>
|
||||
<div class="config-info">
|
||||
<strong>PocketBase Setup:</strong>
|
||||
<div class="config-item">🌐 URL: <code>http://localhost:8090</code></div>
|
||||
<div class="config-item">🔑 Provider: <code>microsoft</code></div>
|
||||
<div class="config-item">📊 Token Expiry: <code>3600 seconds (1 hour)</code></div>
|
||||
<div class="config-item">💾 Storage: <code>localStorage</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div id="statusMessage" class="status-message"></div>
|
||||
|
||||
<!-- Action Section -->
|
||||
<div class="section">
|
||||
<h2>Login</h2>
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" id="loginBtn" onclick="handleLogin()">
|
||||
🚀 Open OAuth Popup
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="clearTokens()">
|
||||
🗑️ Clear Tokens
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Display -->
|
||||
<div id="tokenDisplay" class="token-display">
|
||||
<div class="token-item">
|
||||
<div class="token-label">✓ PocketBase User Token</div>
|
||||
<div class="token-value" id="pbUserToken">Loading...</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-label">✓ PocketBase Agent Token</div>
|
||||
<div class="token-value" id="pbAgentToken">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Info -->
|
||||
<div class="section">
|
||||
<h2>Browser Storage</h2>
|
||||
<div class="info-box">
|
||||
<strong>Tokens stored in localStorage:</strong><br />
|
||||
• <code>auth:pb-user</code> — User OAuth token from Microsoft<br />
|
||||
• <code>auth:pb-agent</code> — Service account token<br />
|
||||
<br />
|
||||
<button class="btn-secondary" onclick="showStorageInfo()" style="width: 100%; margin-top: 12px;">
|
||||
📋 View Storage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug Section -->
|
||||
<div class="section" style="margin-bottom: 0;">
|
||||
<h2>Debug</h2>
|
||||
<div id="debugOutput" class="info-box" style="display: none;"></div>
|
||||
<div class="button-group">
|
||||
<button class="btn-secondary" onclick="showDebugInfo()">
|
||||
🐛 Show Debug Info
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PocketBase SDK -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
|
||||
<!-- OAuth Popup Module (Inline Implementation) -->
|
||||
<script type="module">
|
||||
// Production OAuth Popup Login - Inline Implementation
|
||||
const PRODUCTION_CONFIG = {
|
||||
pbUrl: 'http://localhost:8090',
|
||||
provider: 'microsoft',
|
||||
scopes: ['profile', 'email', 'https://graph.microsoft.com/.default']
|
||||
};
|
||||
|
||||
class OAuthPopupLogin {
|
||||
constructor(overrideConfig = {}) {
|
||||
this.config = {
|
||||
pbUrl: overrideConfig.pbUrl || PRODUCTION_CONFIG.pbUrl,
|
||||
provider: overrideConfig.provider || PRODUCTION_CONFIG.provider,
|
||||
scopes: overrideConfig.scopes || PRODUCTION_CONFIG.scopes,
|
||||
agentEmail: overrideConfig.agentEmail,
|
||||
agentPassword: overrideConfig.agentPassword
|
||||
};
|
||||
this.pb = null;
|
||||
this.popupElement = null;
|
||||
this.uiState = {
|
||||
pbUserToken: null,
|
||||
pbAgentToken: null,
|
||||
status: 'waiting',
|
||||
errorMessage: null
|
||||
};
|
||||
}
|
||||
|
||||
initPocketBase() {
|
||||
if (this.pb) return;
|
||||
const PocketBase = window.PocketBase;
|
||||
if (!PocketBase) {
|
||||
throw new Error('PocketBase SDK not loaded');
|
||||
}
|
||||
this.pb = new PocketBase(this.config.pbUrl);
|
||||
console.log('[OAuthPopup] PocketBase initialized at', this.config.pbUrl);
|
||||
}
|
||||
|
||||
createPopupUI() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'oauth-popup-overlay';
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
padding: 32px;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.oauth-token-badge {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.oauth-token-badge.success {
|
||||
border-color: #10b981;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
.oauth-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; }
|
||||
.oauth-status.pending { background: #f59e0b; animation: pulse 1.5s infinite; }
|
||||
.oauth-status.complete { background: #10b981; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||
.oauth-close-btn { position: absolute; top: 16px; right: 16px; background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280; padding: 0; width: 32px; height: 32px; }
|
||||
.oauth-close-btn:hover { color: #1f2937; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
popup.innerHTML = `
|
||||
<button class="oauth-close-btn" aria-label="Close">×</button>
|
||||
<h2 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600; color: #1f2937;">
|
||||
OAuth Login
|
||||
</h2>
|
||||
<p style="margin: 0 0 24px 0; color: #6b7280; font-size: 14px;">
|
||||
Acquiring tokens through Microsoft OAuth
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label style="display: block; font-size: 12px; font-weight: 600; color: #374151; margin-bottom: 8px;">
|
||||
<span class="oauth-status pending"></span>
|
||||
PocketBase User Token
|
||||
</label>
|
||||
<div id="oauth-pb-user-token" class="oauth-token-badge">
|
||||
Waiting for login...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; font-size: 12px; font-weight: 600; color: #374151; margin-bottom: 8px; margin-top: 16px;">
|
||||
<span class="oauth-status pending"></span>
|
||||
PocketBase Agent Token
|
||||
</label>
|
||||
<div id="oauth-pb-agent-token" class="oauth-token-badge">
|
||||
Will be obtained after user login...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="oauth-status-message" style="margin-top: 20px; padding: 12px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px; color: #1e40af; font-size: 13px;"></div>
|
||||
|
||||
<div id="oauth-error-message" style="margin-top: 20px; padding: 12px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 6px; color: #991b1b; font-size: 13px; display: none;"></div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 24px;">
|
||||
<button id="oauth-login-btn" style="flex: 1; background: #3b82f6; color: white; border: none; border-radius: 6px; padding: 10px 16px; font-weight: 500; cursor: pointer; font-size: 14px;">
|
||||
Start Microsoft Login
|
||||
</button>
|
||||
<button id="oauth-close-btn-bottom" style="flex: 1; background: #e5e7eb; color: #1f2937; border: none; border-radius: 6px; padding: 10px 16px; font-weight: 500; cursor: pointer; font-size: 14px;">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="oauth-success-section" style="display: none; margin-top: 24px; padding: 16px; background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px;">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: #15803d;">
|
||||
✓ Tokens Successfully Acquired
|
||||
</h3>
|
||||
<div id="oauth-final-tokens" style="font-size: 12px;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(popup);
|
||||
|
||||
const closeBtn = popup.querySelector('.oauth-close-btn');
|
||||
const closeBtnBottom = popup.querySelector('#oauth-close-btn-bottom');
|
||||
const loginBtn = popup.querySelector('#oauth-login-btn');
|
||||
|
||||
closeBtn?.addEventListener('click', () => this.close());
|
||||
closeBtnBottom?.addEventListener('click', () => this.close());
|
||||
loginBtn?.addEventListener('click', () => this.performOAuthLogin());
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
updateStatus(status, message) {
|
||||
this.uiState.status = status;
|
||||
const statusEl = document.getElementById('oauth-status-message');
|
||||
const errorEl = document.getElementById('oauth-error-message');
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = message;
|
||||
statusEl.style.display = status === 'error' ? 'none' : 'block';
|
||||
}
|
||||
if (errorEl) {
|
||||
if (status === 'error') {
|
||||
errorEl.textContent = message;
|
||||
errorEl.style.display = 'block';
|
||||
} else {
|
||||
errorEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async performOAuthLogin() {
|
||||
try {
|
||||
this.updateStatus('acquiring', 'Initiating OAuth flow with Microsoft...');
|
||||
console.log('[OAuthPopup] Starting OAuth login flow');
|
||||
|
||||
const authData = await this.pb
|
||||
.collection('users')
|
||||
.authWithOAuth2({
|
||||
provider: this.config.provider,
|
||||
scopes: this.config.scopes
|
||||
});
|
||||
|
||||
console.log('[OAuthPopup] OAuth successful, user:', authData.record.email);
|
||||
|
||||
const pbUserToken = {
|
||||
type: 'pb-user',
|
||||
value: authData.token,
|
||||
expiresAt: Date.now() + 3600 * 1000,
|
||||
acquiredAt: Date.now()
|
||||
};
|
||||
|
||||
this.uiState.pbUserToken = pbUserToken;
|
||||
localStorage.setItem('auth:pb-user', pbUserToken.value);
|
||||
this.updateTokenDisplay('pb-user', pbUserToken);
|
||||
|
||||
this.updateStatus('acquiring', 'User token acquired. Getting agent token...');
|
||||
|
||||
// Agent token is optional in this flow
|
||||
this.updateStatus('complete', '✓ Tokens acquired successfully!');
|
||||
this.displaySuccess();
|
||||
} catch (error) {
|
||||
const errorMsg = error?.message || 'OAuth authentication failed';
|
||||
console.error('[OAuthPopup] Error:', errorMsg);
|
||||
this.updateStatus('error', `Error: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
updateTokenDisplay(type, token) {
|
||||
const elementId = type === 'pb-user' ? 'oauth-pb-user-token' : 'oauth-pb-agent-token';
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
if (!element) return;
|
||||
|
||||
const expiresAt = new Date(token.expiresAt).toLocaleString();
|
||||
element.classList.add('success');
|
||||
element.innerHTML = `
|
||||
<strong>${token.value}</strong><br>
|
||||
<small>Expires: ${expiresAt}</small>
|
||||
`;
|
||||
}
|
||||
|
||||
displaySuccess() {
|
||||
const successSection = document.getElementById('oauth-success-section');
|
||||
const loginBtn = document.getElementById('oauth-login-btn');
|
||||
|
||||
if (successSection) {
|
||||
successSection.style.display = 'block';
|
||||
const finalTokens = document.getElementById('oauth-final-tokens');
|
||||
if (finalTokens && this.uiState.pbUserToken) {
|
||||
const pbUserDisplay = `<div><strong>PB User Token:</strong><br><code style="font-size: 10px;">${this.uiState.pbUserToken.value.substring(0, 60)}...</code></div>`;
|
||||
const pbAgentDisplay = this.uiState.pbAgentToken
|
||||
? `<div><strong>PB Agent Token:</strong><br><code style="font-size: 10px;">${this.uiState.pbAgentToken.value.substring(0, 60)}...</code></div>`
|
||||
: '';
|
||||
finalTokens.innerHTML = pbUserDisplay + pbAgentDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
if (loginBtn) {
|
||||
loginBtn.textContent = 'Tokens Acquired ✓';
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.style.opacity = '0.5';
|
||||
}
|
||||
}
|
||||
|
||||
async open() {
|
||||
this.initPocketBase();
|
||||
this.popupElement = this.createPopupUI();
|
||||
document.body.appendChild(this.popupElement);
|
||||
|
||||
this.popupElement.addEventListener('click', (e) => {
|
||||
if (e.target === this.popupElement) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.popupElement) {
|
||||
this.popupElement.remove();
|
||||
this.popupElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
getTokens() {
|
||||
return {
|
||||
pbUser: this.uiState.pbUserToken,
|
||||
pbAgent: this.uiState.pbAgentToken
|
||||
};
|
||||
}
|
||||
|
||||
getState() {
|
||||
return { ...this.uiState };
|
||||
}
|
||||
}
|
||||
|
||||
// Export to global for page functions
|
||||
window.OAuthPopupLogin = OAuthPopupLogin;
|
||||
</script>
|
||||
|
||||
<!-- Page Functions -->
|
||||
<script>
|
||||
let loginPopup = null;
|
||||
|
||||
async function handleLogin() {
|
||||
try {
|
||||
showStatus('Opening OAuth popup...', 'info');
|
||||
loginPopup = new window.OAuthPopupLogin();
|
||||
await loginPopup.open();
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
const state = loginPopup?.getState?.();
|
||||
if (state?.pbUserToken) {
|
||||
clearInterval(checkInterval);
|
||||
displayTokens();
|
||||
showStatus('Tokens acquired! Check localStorage keys: auth:pb-user and auth:pb-agent', 'success');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => clearInterval(checkInterval), 300000);
|
||||
} catch (error) {
|
||||
showStatus(`Error: ${error.message}`, 'error');
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayTokens() {
|
||||
if (!loginPopup) return;
|
||||
|
||||
const tokens = loginPopup.getTokens();
|
||||
const display = document.getElementById('tokenDisplay');
|
||||
|
||||
if (tokens.pbUser) {
|
||||
document.getElementById('pbUserToken').textContent = tokens.pbUser.value;
|
||||
}
|
||||
if (tokens.pbAgent) {
|
||||
document.getElementById('pbAgentToken').textContent = tokens.pbAgent.value;
|
||||
} else {
|
||||
document.getElementById('pbAgentToken').textContent = 'Not acquired in this flow';
|
||||
}
|
||||
|
||||
display.classList.add('visible');
|
||||
}
|
||||
|
||||
function clearTokens() {
|
||||
localStorage.removeItem('auth:pb-user');
|
||||
localStorage.removeItem('auth:pb-agent');
|
||||
document.getElementById('pbUserToken').textContent = 'Not acquired';
|
||||
document.getElementById('pbAgentToken').textContent = 'Not acquired';
|
||||
document.getElementById('tokenDisplay').classList.remove('visible');
|
||||
showStatus('Tokens cleared from localStorage', 'success');
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const statusEl = document.getElementById('statusMessage');
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `status-message ${type}`;
|
||||
}
|
||||
|
||||
function showStorageInfo() {
|
||||
const pbUser = localStorage.getItem('auth:pb-user');
|
||||
const pbAgent = localStorage.getItem('auth:pb-agent');
|
||||
|
||||
const output = `
|
||||
<strong>localStorage Contents:</strong><br>
|
||||
<code>auth:pb-user</code>: ${pbUser ? pbUser.substring(0, 60) + '...' : '(not set)'}<br>
|
||||
<code>auth:pb-agent</code>: ${pbAgent ? pbAgent.substring(0, 60) + '...' : '(not set)'}
|
||||
`;
|
||||
|
||||
const debugOutput = document.getElementById('debugOutput');
|
||||
debugOutput.innerHTML = output;
|
||||
debugOutput.style.display = 'block';
|
||||
}
|
||||
|
||||
function showDebugInfo() {
|
||||
const output = `
|
||||
<strong>OAuth Popup State:</strong><br>
|
||||
${loginPopup ? JSON.stringify(loginPopup.getState(), null, 2) : 'No popup created yet'}<br><br>
|
||||
<strong>Browser Storage:</strong><br>
|
||||
auth:pb-user: ${localStorage.getItem('auth:pb-user') ? '✓ Set' : '✗ Not set'}<br>
|
||||
auth:pb-agent: ${localStorage.getItem('auth:pb-agent') ? '✓ Set' : '✗ Not set'}
|
||||
`;
|
||||
|
||||
const debugOutput = document.getElementById('debugOutput');
|
||||
debugOutput.innerHTML = output;
|
||||
debugOutput.style.display = 'block';
|
||||
}
|
||||
|
||||
// Check on load
|
||||
window.addEventListener('load', () => {
|
||||
if (localStorage.getItem('auth:pb-user')) {
|
||||
displayTokens();
|
||||
showStatus('Existing tokens found in localStorage', 'success');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user