c0d32d7658
- Changed title to Mode1Svelte throughout frontend - Added mode indicator in bottom left with pulsing green dot - All tests passing and dashboard working
587 lines
17 KiB
HTML
587 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mode1Svelte - Auth Module Test</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: #0f172a;
|
|
color: #e2e8f0;
|
|
padding: 2rem;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
margin-bottom: 2rem;
|
|
color: #f1f5f9;
|
|
text-align: center;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.section {
|
|
background: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.section h2 {
|
|
font-size: 1.25rem;
|
|
margin-bottom: 1rem;
|
|
color: #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.status-icon {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.status-icon.success {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.status-icon.pending {
|
|
background: #f59e0b;
|
|
}
|
|
|
|
.status-icon.error {
|
|
background: #ef4444;
|
|
}
|
|
|
|
button {
|
|
background: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.95rem;
|
|
transition: background 0.2s;
|
|
width: 100%;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
button:hover {
|
|
background: #2563eb;
|
|
}
|
|
|
|
button:disabled {
|
|
background: #64748b;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.button-group {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.output {
|
|
background: #0f172a;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 1rem;
|
|
margin-top: 1rem;
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.85rem;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.output.success {
|
|
border-color: #22c55e;
|
|
color: #22c55e;
|
|
}
|
|
|
|
.output.error {
|
|
border-color: #ef4444;
|
|
color: #ef4444;
|
|
}
|
|
|
|
.output.warning {
|
|
border-color: #f59e0b;
|
|
color: #f59e0b;
|
|
}
|
|
|
|
.info {
|
|
background: #1e293b;
|
|
border-left: 4px solid #3b82f6;
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-bottom: 1rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.timestamp {
|
|
font-size: 0.8rem;
|
|
color: #64748b;
|
|
margin-top: 1rem;
|
|
text-align: right;
|
|
}
|
|
|
|
.mode-indicator {
|
|
position: fixed;
|
|
bottom: 1rem;
|
|
left: 1rem;
|
|
background: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 6px;
|
|
padding: 0.75rem 1rem;
|
|
font-size: 0.875rem;
|
|
color: #94a3b8;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.mode-indicator .mode-name {
|
|
font-weight: 600;
|
|
color: #f1f5f9;
|
|
}
|
|
|
|
.mode-indicator .mode-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #22c55e;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.button-group {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🔐 Auth Module Test Suite (Mode1Svelte)</h1>
|
|
|
|
<div class="grid">
|
|
<!-- Frontend Auth Section -->
|
|
<div class="section">
|
|
<h2>
|
|
<span class="status-icon pending" id="frontendStatus"></span>
|
|
Frontend: PocketBase Auth
|
|
</h2>
|
|
|
|
<div class="info">
|
|
Tests OAuth2 login, token refresh, and user state management.
|
|
</div>
|
|
|
|
<div class="button-group">
|
|
<button onclick="testFrontendInit()">Initialize Auth</button>
|
|
<button onclick="testGetAuthState()">Get Auth State</button>
|
|
</div>
|
|
<button onclick="testFrontendLogout()">Logout</button>
|
|
|
|
<div class="output" id="frontendOutput"></div>
|
|
<div class="timestamp" id="frontendTime"></div>
|
|
</div>
|
|
|
|
<!-- Backend Auth Section -->
|
|
<div class="section">
|
|
<h2>
|
|
<span class="status-icon pending" id="backendStatus"></span>
|
|
Backend: Graph Token & PB Validation
|
|
</h2>
|
|
|
|
<div class="info">
|
|
Tests Microsoft Graph token acquisition, caching, and PocketBase token validation.
|
|
</div>
|
|
|
|
<div class="button-group">
|
|
<button onclick="testGraphToken()">Get Graph Token</button>
|
|
<button onclick="testGraphRefresh()">Refresh Graph Token</button>
|
|
</div>
|
|
<button onclick="testCompareTokens()">Compare PB vs Graph</button>
|
|
<button onclick="testPBValidation()">Validate PB Token</button>
|
|
<button onclick="testCompareTokens()">Compare PB vs Graph</button>
|
|
<div class="output" id="backendOutput"></div>
|
|
<div class="timestamp" id="backendTime"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Job Info Section -->
|
|
<div class="section">
|
|
<h2>
|
|
<span class="status-icon pending" id="jobInfoStatus"></span>
|
|
Job Info (First 4 Records)
|
|
</h2>
|
|
|
|
<div class="info">
|
|
Fetch and display first 4 records from Job_Info_Prod collection.
|
|
</div>
|
|
|
|
<button onclick="testJobInfo()">Fetch Job Info</button>
|
|
|
|
<div class="output" id="jobInfoOutput"></div>
|
|
<div class="timestamp" id="jobInfoTime"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login container (shown when not authenticated) -->
|
|
<div id="loginContainer" class="hidden">
|
|
<button id="loginBtn">Login with Microsoft</button>
|
|
<div id="loginError" class="hidden"></div>
|
|
</div>
|
|
|
|
<!-- User display (shown when authenticated) -->
|
|
<div id="userDisplayName"></div>
|
|
<div id="userEmailValue"></div>
|
|
|
|
<script src="/auth-module.js"></script>
|
|
<script>
|
|
let pbAuth = null;
|
|
let config = null;
|
|
|
|
// Utility: Log to output
|
|
function log(elementId, message, type = 'info') {
|
|
const output = document.getElementById(elementId);
|
|
if (!output) return;
|
|
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const line = `[${timestamp}] ${message}`;
|
|
|
|
output.textContent += (output.textContent ? '\n' : '') + line;
|
|
output.scrollTop = output.scrollHeight;
|
|
output.className = `output ${type}`;
|
|
|
|
// Update timestamp
|
|
const timeEl = document.getElementById(elementId.replace('Output', 'Time'));
|
|
if (timeEl) timeEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
|
|
}
|
|
|
|
function setStatus(elementId, status) {
|
|
const icon = document.getElementById(elementId);
|
|
if (!icon) return;
|
|
icon.className = `status-icon ${status}`;
|
|
}
|
|
|
|
// Test: Load config
|
|
async function loadConfig() {
|
|
try {
|
|
const response = await fetch('./api/auth/config');
|
|
if (!response.ok) throw new Error('Failed to load config');
|
|
config = await response.json();
|
|
document.getElementById('configOutput').textContent = JSON.stringify(config, null, 2);
|
|
document.getElementById('configOutput').className = 'output success';
|
|
} catch (error) {
|
|
log('configOutput', `Error loading config: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Test: Initialize Frontend Auth
|
|
async function testFrontendInit() {
|
|
setStatus('frontendStatus', 'pending');
|
|
log('frontendOutput', 'Initializing PocketBaseAuth...');
|
|
|
|
try {
|
|
if (typeof window.AuthModule === 'undefined' || !window.AuthModule.initPocketBaseAuth) {
|
|
throw new Error('Auth module not loaded');
|
|
}
|
|
|
|
pbAuth = await window.AuthModule.initPocketBaseAuth('.', {
|
|
onAuthSuccess: (state) => {
|
|
log('frontendOutput', `✓ Auth success: ${state.user?.email}`, 'success');
|
|
setStatus('frontendStatus', 'success');
|
|
},
|
|
onAuthFailure: (error) => {
|
|
log('frontendOutput', `✗ Auth failed: ${error.message}`, 'error');
|
|
setStatus('frontendStatus', 'error');
|
|
},
|
|
onTokenUpdate: (state) => {
|
|
log('frontendOutput', `✓ Token refreshed for ${state.user?.email}`, 'success');
|
|
},
|
|
onUiUpdate: (state) => {
|
|
log('frontendOutput', `✓ UI updated: ${state.isAuthenticated ? 'Authenticated' : 'Not authenticated'}`);
|
|
},
|
|
});
|
|
|
|
log('frontendOutput', '✓ Frontend auth initialized', 'success');
|
|
setStatus('frontendStatus', 'success');
|
|
} catch (error) {
|
|
log('frontendOutput', `✗ Initialization failed: ${error.message}`, 'error');
|
|
setStatus('frontendStatus', 'error');
|
|
}
|
|
}
|
|
|
|
// Test: Get Auth State
|
|
function testGetAuthState() {
|
|
if (!pbAuth) {
|
|
log('frontendOutput', '✗ Auth not initialized', 'error');
|
|
return;
|
|
}
|
|
|
|
const state = pbAuth.getAuthState();
|
|
log('frontendOutput', `Auth State: ${JSON.stringify(state, null, 2)}`, 'success');
|
|
}
|
|
|
|
// Test: Frontend Logout
|
|
function testFrontendLogout() {
|
|
if (!pbAuth) {
|
|
log('frontendOutput', '✗ Auth not initialized', 'error');
|
|
return;
|
|
}
|
|
|
|
pbAuth.logout();
|
|
log('frontendOutput', '✓ Logged out', 'success');
|
|
setStatus('frontendStatus', 'pending');
|
|
}
|
|
|
|
// Test: Get Graph Token
|
|
async function testGraphToken() {
|
|
setStatus('backendStatus', 'pending');
|
|
log('backendOutput', 'Requesting Graph token from backend...');
|
|
|
|
try {
|
|
const response = await fetch('./api/auth/graph/token');
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
let output = `✓ Token acquired\n`;
|
|
output += `Expires: ${data.expiresOn}\n`;
|
|
output += `Valid in cache: ${data.isValid}\n\n`;
|
|
|
|
if (data.tokenDetails && typeof data.tokenDetails === 'object') {
|
|
output += `Token Details:\n`;
|
|
output += ` Audience: ${data.tokenDetails.aud}\n`;
|
|
output += ` Issuer: ${data.tokenDetails.iss}\n`;
|
|
output += ` Scopes: ${data.tokenDetails.scp}\n`;
|
|
output += ` App: ${data.tokenDetails.app_displayname}\n`;
|
|
output += ` Issued: ${data.tokenDetails.iat}\n`;
|
|
output += ` Expires: ${data.tokenDetails.exp}\n\n`;
|
|
}
|
|
|
|
output += `Full Token (first 100 chars):\n${data.fullToken.substring(0, 100)}...`;
|
|
|
|
log('backendOutput', output, 'success');
|
|
setStatus('backendStatus', 'success');
|
|
} else {
|
|
log('backendOutput', `✗ ${data.error}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
} catch (error) {
|
|
log('backendOutput', `✗ Request failed: ${error.message}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
}
|
|
|
|
// Test: Refresh Graph Token
|
|
async function testGraphRefresh() {
|
|
setStatus('backendStatus', 'pending');
|
|
log('backendOutput', 'Refreshing Graph token...');
|
|
|
|
try {
|
|
const response = await fetch('./api/auth/refresh-graph', { method: 'POST' });
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
log('backendOutput', `✓ Token refreshed\nExpires: ${data.expiresOn}`, 'success');
|
|
setStatus('backendStatus', 'success');
|
|
} else {
|
|
log('backendOutput', `✗ ${data.error}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
} catch (error) {
|
|
log('backendOutput', `✗ Refresh failed: ${error.message}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
}
|
|
|
|
// Test: Compare tokens
|
|
async function testCompareTokens() {
|
|
setStatus('backendStatus', 'pending');
|
|
log('backendOutput', 'Comparing PocketBase token vs Graph token...');
|
|
|
|
if (!pbAuth) {
|
|
log('backendOutput', '✗ Auth not initialized', 'error');
|
|
setStatus('backendStatus', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const state = pbAuth.getAuthState();
|
|
const pbToken = state.token || null;
|
|
|
|
const response = await fetch('./api/auth/compare-tokens', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pbToken }),
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.comparison) {
|
|
let output = `✓ Token Comparison:\n\n`;
|
|
|
|
output += `GRAPH TOKEN (Backend/App-only):\n`;
|
|
output += ` Type: ${data.comparison.graph_token.type}\n`;
|
|
output += ` Source: ${data.comparison.graph_token.source}\n`;
|
|
output += ` Issuer: ${data.comparison.graph_token.issuer}\n`;
|
|
output += ` Audience: ${data.comparison.graph_token.audience}\n`;
|
|
output += ` App: ${data.comparison.graph_token.app}\n`;
|
|
output += ` Scopes: ${data.comparison.graph_token.scopes}\n`;
|
|
output += ` Purpose: ${data.comparison.graph_token.purpose}\n\n`;
|
|
|
|
output += `POCKETBASE TOKEN (Frontend/User):\n`;
|
|
output += ` Type: ${data.comparison.pocketbase_token.type}\n`;
|
|
output += ` Source: ${data.comparison.pocketbase_token.source}\n`;
|
|
output += ` User: ${data.comparison.pocketbase_token.authenticated_user}\n`;
|
|
output += ` Purpose: ${data.comparison.pocketbase_token.purpose}\n\n`;
|
|
|
|
output += `COMPARISON:\n`;
|
|
output += ` Are Different: YES ✓\n`;
|
|
output += ` ${data.comparison.explanation}\n`;
|
|
|
|
log('backendOutput', output, 'success');
|
|
setStatus('backendStatus', 'success');
|
|
} else {
|
|
log('backendOutput', `✗ ${data.error}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
} catch (error) {
|
|
log('backendOutput', `✗ Comparison failed: ${error.message}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
}
|
|
|
|
// Test: Validate PocketBase Token
|
|
async function testPBValidation() {
|
|
setStatus('backendStatus', 'pending');
|
|
log('backendOutput', 'Validating PocketBase token...');
|
|
|
|
if (!pbAuth) {
|
|
log('backendOutput', '✗ Auth not initialized', 'error');
|
|
setStatus('backendStatus', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const state = pbAuth.getAuthState();
|
|
|
|
if (!state.token) {
|
|
log('backendOutput', '✗ No token available - user not logged in', 'warning');
|
|
setStatus('backendStatus', 'warning');
|
|
return;
|
|
}
|
|
|
|
log('backendOutput', `Sending token for user: ${state.user?.email}...`);
|
|
|
|
const response = await fetch('./api/auth/validate-pb-token', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pbToken: state.token }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.valid) {
|
|
log('backendOutput', `✓ Token valid\nUser: ${data.user?.email}\nID: ${data.user?.id}`, 'success');
|
|
setStatus('backendStatus', 'success');
|
|
} else {
|
|
log('backendOutput', `✗ Token invalid: ${data.error}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
} catch (error) {
|
|
log('backendOutput', `✗ Validation failed: ${error.message}`, 'error');
|
|
setStatus('backendStatus', 'error');
|
|
}
|
|
}
|
|
|
|
// Test: Fetch Job Info
|
|
async function testJobInfo() {
|
|
setStatus('jobInfoStatus', 'pending');
|
|
log('jobInfoOutput', 'Fetching first 4 records from Job_Info_Prod...');
|
|
|
|
if (!pbAuth) {
|
|
log('jobInfoOutput', '✗ Auth not initialized - cannot fetch records', 'error');
|
|
setStatus('jobInfoStatus', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const pb = pbAuth.getPocketBase();
|
|
const records = await pb.collection('Job_Info_Prod').getList(1, 4);
|
|
|
|
if (!records.items || records.items.length === 0) {
|
|
log('jobInfoOutput', 'No records found', 'warning');
|
|
setStatus('jobInfoStatus', 'warning');
|
|
return;
|
|
}
|
|
|
|
let output = `✓ Found ${records.items.length} records:\n\n`;
|
|
|
|
records.items.forEach((record, index) => {
|
|
output += `[${index + 1}] ID: ${record.id}\n`;
|
|
output += ` Title: ${record.title || record.name || 'N/A'}\n`;
|
|
output += ` Status: ${record.status || 'N/A'}\n`;
|
|
output += ` Created: ${new Date(record.created).toLocaleDateString()}\n\n`;
|
|
});
|
|
|
|
log('jobInfoOutput', output, 'success');
|
|
setStatus('jobInfoStatus', 'success');
|
|
} catch (error) {
|
|
log('jobInfoOutput', `✗ Failed to fetch records: ${error.message}`, 'error');
|
|
setStatus('jobInfoStatus', 'error');
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Mode Indicator -->
|
|
<div class="mode-indicator">
|
|
<div class="mode-dot"></div>
|
|
<span class="mode-name">Mode1Svelte</span>
|
|
</div>
|
|
</body>
|
|
</html>
|