Add Mode1Svelte - independent copy of Mode1 for future SvelteKit integration
- Created Mode1Svelte as complete standalone project - Identical to Mode1 currently (Hono backend + Vite frontend) - Separate auth-module copy (no dependencies on Mode1) - Updated orchestrator to default to Mode1Svelte - Both Mode1 and Mode1Svelte are independent and interchangeable via ModeSwitch - Ready for gradual Svelte/SvelteKit migration while maintaining API compatibility
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { cors } from 'hono/cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { AuthConfigManager, BackendAuth } from '../auth-module/backend';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const app = new Hono();
|
||||
const PORT = process.env.PORT || 3005;
|
||||
|
||||
// Initialize auth
|
||||
const backendAuth = new BackendAuth({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
}, process.env.PB_DB);
|
||||
|
||||
// CORS Configuration - Allow requests from frontend domains
|
||||
app.use('*', cors({
|
||||
origin: ['https://ji-test.ccllc.pro', 'http://localhost:3005', 'http://localhost:5173'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Serve bundled auth module JavaScript
|
||||
app.get('/auth-module.js', async (c) => {
|
||||
const filePath = path.join(__dirname, '../frontend/dist/auth.js');
|
||||
if (existsSync(filePath)) {
|
||||
const content = await readFile(filePath);
|
||||
c.header('Content-Type', 'application/javascript');
|
||||
return c.body(content);
|
||||
}
|
||||
return c.notFound();
|
||||
});
|
||||
|
||||
// Serve frontend static files
|
||||
app.use('/', serveStatic({ root: path.join(__dirname, '../frontend') }));
|
||||
|
||||
// API: Get frontend config
|
||||
app.get('/api/auth/config', (c) => {
|
||||
try {
|
||||
const config = AuthConfigManager.getFrontendConfig();
|
||||
return c.json(config);
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ error: 'Failed to get config', message: error instanceof Error ? error.message : 'Unknown error' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Get Graph token status
|
||||
app.get('/api/auth/graph/token', async (c) => {
|
||||
try {
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!token) {
|
||||
return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500);
|
||||
}
|
||||
|
||||
// Decode JWT to show token details (for verification)
|
||||
const parts = token.split('.');
|
||||
let payload = {};
|
||||
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 c.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 c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to acquire token' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Validate PocketBase token
|
||||
app.post('/api/auth/validate-pb-token', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const token = body.pbToken;
|
||||
|
||||
if (!token) {
|
||||
return c.json({ valid: false, error: 'No token provided' }, 400);
|
||||
}
|
||||
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(token);
|
||||
const user = isValid ? await backendAuth.pbValidator.getUserRecord(token) : null;
|
||||
|
||||
return c.json({
|
||||
valid: isValid,
|
||||
user: user ? {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
} : null,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ valid: false, error: error instanceof Error ? error.message : 'Validation failed' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Refresh Graph token (test cache)
|
||||
app.post('/api/auth/refresh-graph', async (c) => {
|
||||
try {
|
||||
backendAuth.graphTokenManager.clearCache();
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
return c.json({
|
||||
success: true,
|
||||
refreshed: true,
|
||||
expiresOn: expiresOnISO,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Refresh failed' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Compare PB and Graph tokens
|
||||
app.post('/api/auth/compare-tokens', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const pbToken = body.pbToken;
|
||||
|
||||
// Get Graph token
|
||||
const { token: graphToken, expiresOnISO: graphExpires } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!graphToken) {
|
||||
return c.json({ success: false, error: 'Failed to acquire Graph token' }, 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 c.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 c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to compare tokens' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
console.log(`Mode1 Auth Test Server running on port ${PORT}`);
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
Reference in New Issue
Block a user