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:
2026-01-23 03:00:47 +00:00
parent 7b997dc354
commit 7d8fd9f251
28 changed files with 4322 additions and 4 deletions
+12
View File
@@ -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()}
+259
View File
@@ -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 }
);
}
};