Add Mode2Svelte - advanced SvelteKit implementation
- Created Mode2Svelte as a copy of Mode1Svelte - Updated branding (title, description) to distinguish from Mode1Svelte - Added Mode2Svelte support to orchestrator - Configured orchestrator to spawn Mode2Svelte with Node.js adapter - Set Mode2Svelte as active mode in activeMode.txt - Built production bundle for Mode2Svelte - Both Mode1Svelte and Mode2Svelte now managed by orchestrator on port 3005 - Orchestrator controls which mode runs via activeMode.txt configuration
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
@@ -0,0 +1,259 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { PocketBaseAuth } from '$lib/auth/frontend';
|
||||
import type { AuthState } from '$lib/auth/types';
|
||||
|
||||
let auth: PocketBaseAuth | undefined;
|
||||
let authState: AuthState = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
let graphToken = '';
|
||||
let pbTokenValid = false;
|
||||
let comparisonData: string | null = null;
|
||||
let loading = false;
|
||||
let error = '';
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/config');
|
||||
if (!response.ok) throw new Error('Failed to get config');
|
||||
const config = await response.json();
|
||||
|
||||
auth = new PocketBaseAuth(config, {
|
||||
onAuthSuccess: (state: AuthState) => {
|
||||
authState = state;
|
||||
console.log('Auth success:', state);
|
||||
},
|
||||
onAuthFailure: (err: unknown) => {
|
||||
error = 'Authentication failed: ' + (err instanceof Error ? err.message : 'Unknown error');
|
||||
console.error('Auth error:', err);
|
||||
},
|
||||
onTokenUpdate: (state: AuthState) => {
|
||||
authState = state;
|
||||
},
|
||||
onUiUpdate: (state: AuthState) => {
|
||||
authState = state;
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleFetchGraphToken() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api/auth/graph/token');
|
||||
if (!response.ok) throw new Error('Failed to fetch Graph token');
|
||||
const data = await response.json();
|
||||
graphToken = JSON.stringify(data, null, 2);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unknown error';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefreshGraphToken() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api/auth/refresh-graph', { method: 'POST' });
|
||||
if (!response.ok) throw new Error('Failed to refresh');
|
||||
const data = await response.json();
|
||||
graphToken = JSON.stringify(data, null, 2);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unknown error';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleValidatePBToken() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const pbToken = auth?.getPocketBase?.()?.authStore?.token;
|
||||
if (!pbToken) {
|
||||
error = 'No PocketBase token found. Please log in first.';
|
||||
pbTokenValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/validate-pb-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken }),
|
||||
});
|
||||
if (!response.ok) throw new Error('Validation failed');
|
||||
const data = await response.json();
|
||||
pbTokenValid = data.valid;
|
||||
error = data.valid ? '' : 'Token validation failed';
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unknown error';
|
||||
pbTokenValid = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCompareTokens() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const pbToken = auth?.getPocketBase?.()?.authStore?.token;
|
||||
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('Comparison failed');
|
||||
const data = await response.json();
|
||||
comparisonData = JSON.stringify(data, null, 2);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Unknown error';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
if (auth) {
|
||||
auth.logout();
|
||||
authState = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
error = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="min-height: 100vh; background: linear-gradient(to bottom, #0f172a, #1e293b); color: white; padding: 16px; padding-bottom: 100px;">
|
||||
<div style="max-width: 800px; margin: 0 auto;">
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom: 32px;">
|
||||
<h1 style="font-size: 28px; font-weight: bold; margin: 0 0 8px 0; color: #60a5fa;">Mode2Svelte</h1>
|
||||
<p style="font-size: 14px; color: #94a3b8; margin: 0;">Advanced Auth Testing Dashboard</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Display -->
|
||||
{#if error}
|
||||
<div style="background: rgba(127, 29, 29, 0.2); border: 1px solid #dc2626; color: #fca5a5; padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 14px;">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Auth Status -->
|
||||
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
|
||||
<h2 style="font-size: 20px; font-weight: bold; margin: 0 0 12px 0;">Authentication Status</h2>
|
||||
|
||||
{#if authState.isAuthenticated && authState.user !== null}
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="font-size: 14px;">
|
||||
<span style="color: #cbd5e1;">Status:</span>
|
||||
<span style="margin-left: 8px; color: #4ade80; font-weight: bold;">✓ Authenticated</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; word-break: break-all;">
|
||||
<span style="color: #cbd5e1;">User:</span>
|
||||
<span style="margin-left: 8px; color: #93c5fd;" id="userDisplayName">{authState.user?.name}</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; word-break: break-all;">
|
||||
<span style="color: #cbd5e1;">Email:</span>
|
||||
<span style="margin-left: 8px; color: #93c5fd;" id="userEmailValue">{authState.user?.email}</span>
|
||||
</div>
|
||||
<button
|
||||
on:click={handleLogout}
|
||||
style="background: #dc2626; color: white; border: none; padding: 12px 16px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; margin-top: 8px; width: 100%;"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div id="loginContainer" style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<p style="font-size: 14px; color: #cbd5e1; margin: 0;">Not authenticated. Log in to begin testing.</p>
|
||||
<button
|
||||
id="loginBtn"
|
||||
style="background: #2563eb; color: white; border: none; padding: 16px; border-radius: 6px; font-weight: bold; font-size: 16px; cursor: pointer; width: 100%;"
|
||||
>
|
||||
Login with Microsoft
|
||||
</button>
|
||||
<div id="loginError" style="color: #f87171; font-size: 12px; display: none;"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- API Testing -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;">
|
||||
<!-- Graph Token -->
|
||||
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px;">
|
||||
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">Graph Token</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<button
|
||||
on:click={handleFetchGraphToken}
|
||||
disabled={loading}
|
||||
style="background: #4f46e5; color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Fetch Token'}
|
||||
</button>
|
||||
<button
|
||||
on:click={handleRefreshGraphToken}
|
||||
disabled={loading}
|
||||
style="background: #4f46e5; color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PocketBase Token -->
|
||||
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px;">
|
||||
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">PocketBase Token</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<button
|
||||
on:click={handleValidatePBToken}
|
||||
disabled={loading}
|
||||
style="background: #059669; color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Validate'}
|
||||
</button>
|
||||
{#if pbTokenValid}
|
||||
<p style="color: #4ade80; font-size: 12px; margin: 0;">✓ Token is valid</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compare Tokens -->
|
||||
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||||
<button
|
||||
on:click={handleCompareTokens}
|
||||
disabled={loading}
|
||||
style="background: #9333ea; color: white; border: none; padding: 12px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Compare Tokens'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Output -->
|
||||
{#if graphToken}
|
||||
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||||
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">Graph Token Response</h3>
|
||||
<pre style="background: #0f172a; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 11px; color: #cbd5e1; max-height: 200px; white-space: pre-wrap; word-wrap: break-word; margin: 0;">{graphToken}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if comparisonData}
|
||||
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px;">
|
||||
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">Token Comparison</h3>
|
||||
<pre style="background: #0f172a; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 11px; color: #cbd5e1; max-height: 200px; white-space: pre-wrap; word-wrap: break-word; margin: 0;">{comparisonData}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { BackendAuth } from '$lib/auth/backend';
|
||||
|
||||
const backendAuth = new BackendAuth(
|
||||
{
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
},
|
||||
process.env.PB_DB
|
||||
);
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const pbToken = body.pbToken;
|
||||
|
||||
// Get Graph token
|
||||
const { token: graphToken, expiresOnISO: graphExpires } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!graphToken) {
|
||||
return json({ success: false, error: 'Failed to acquire Graph token' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Decode both tokens to compare
|
||||
const decodeJWT = (token: string) => {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
return JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const graphPayload = decodeJWT(graphToken);
|
||||
|
||||
// Validate PB token to get actual user
|
||||
let authenticatedUser = 'Unknown';
|
||||
if (pbToken) {
|
||||
try {
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(pbToken);
|
||||
if (isValid) {
|
||||
const userRecord = await backendAuth.pbValidator.getUserRecord(pbToken);
|
||||
authenticatedUser = userRecord?.email || userRecord?.id || 'Unknown';
|
||||
}
|
||||
} catch {
|
||||
authenticatedUser = 'Token invalid or expired';
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
comparison: {
|
||||
graph_token: {
|
||||
issuer: graphPayload?.iss || 'N/A',
|
||||
audience: graphPayload?.aud || 'N/A',
|
||||
app: graphPayload?.app_displayname || 'N/A',
|
||||
scopes: graphPayload?.scp || 'N/A',
|
||||
issued_at: graphPayload?.iat ? new Date(graphPayload.iat * 1000).toISOString() : 'N/A',
|
||||
expires_at: graphPayload?.exp ? new Date(graphPayload.exp * 1000).toISOString() : 'N/A',
|
||||
type: 'Microsoft Graph Token (Backend/App-only)',
|
||||
source: '@azure/msal-node',
|
||||
purpose: 'Backend API calls to Microsoft Graph',
|
||||
},
|
||||
pocketbase_token: {
|
||||
note: 'User token from PocketBase OAuth2',
|
||||
type: 'PocketBase User Token (Frontend/User)',
|
||||
source: 'PocketBase OAuth2 flow',
|
||||
purpose: 'User authentication and authorization',
|
||||
authenticated_user: authenticatedUser,
|
||||
},
|
||||
are_different: true,
|
||||
explanation: 'These are completely different tokens from different systems: Graph token is for app-to-app Microsoft API access, PocketBase token is for user authentication in the application.',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to compare tokens' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { AuthConfigManager } from '$lib/auth/backend';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const config = AuthConfigManager.getFrontendConfig();
|
||||
return json(config);
|
||||
} catch (error) {
|
||||
return json(
|
||||
{ error: 'Failed to get config', message: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { BackendAuth, AuthConfigManager } from '$lib/auth/backend';
|
||||
|
||||
const backendAuth = new BackendAuth(
|
||||
{
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
},
|
||||
process.env.PB_DB
|
||||
);
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
try {
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!token) {
|
||||
return json({ success: false, error: 'Failed to acquire Graph token' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Decode JWT to show token details (for verification)
|
||||
const parts = token.split('.');
|
||||
let payload: any = {};
|
||||
if (parts.length === 3) {
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
payload = {
|
||||
aud: decoded.aud,
|
||||
iss: decoded.iss,
|
||||
scp: decoded.scp,
|
||||
app_displayname: decoded.app_displayname,
|
||||
iat: new Date(decoded.iat * 1000).toISOString(),
|
||||
exp: new Date(decoded.exp * 1000).toISOString(),
|
||||
};
|
||||
} catch (e) {
|
||||
// Silent fail - just show truncated token
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
token: token.substring(0, 50) + '...',
|
||||
fullToken: token, // Include full token for testing
|
||||
expiresOn: expiresOnISO,
|
||||
isValid: backendAuth.graphTokenManager.isTokenValid(),
|
||||
tokenDetails: Object.keys(payload).length > 0 ? payload : 'Could not decode',
|
||||
});
|
||||
} catch (error) {
|
||||
return json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to acquire token' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { BackendAuth } from '$lib/auth/backend';
|
||||
|
||||
const backendAuth = new BackendAuth(
|
||||
{
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
},
|
||||
process.env.PB_DB
|
||||
);
|
||||
|
||||
export const POST: RequestHandler = async () => {
|
||||
try {
|
||||
backendAuth.graphTokenManager.clearCache();
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
return json({
|
||||
success: true,
|
||||
refreshed: true,
|
||||
expiresOn: expiresOnISO,
|
||||
});
|
||||
} catch (error) {
|
||||
return json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Refresh failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { BackendAuth } from '$lib/auth/backend';
|
||||
|
||||
const backendAuth = new BackendAuth(
|
||||
{
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
},
|
||||
process.env.PB_DB
|
||||
);
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const token = body.pbToken;
|
||||
|
||||
if (!token) {
|
||||
return json({ valid: false, error: 'No token provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(token);
|
||||
const user = isValid ? await backendAuth.pbValidator.getUserRecord(token) : null;
|
||||
|
||||
return json({
|
||||
valid: isValid,
|
||||
user: user ? {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
} : null,
|
||||
});
|
||||
} catch (error) {
|
||||
return json(
|
||||
{ valid: false, error: error instanceof Error ? error.message : 'Validation failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user