Files
2026-01-10 19:23:47 +00:00

669 lines
20 KiB
HTML

<!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">&times;</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>