Mode2Test AuthAndToken component complete: login/logout working, token persistence, background refresh active, tested and verified

This commit is contained in:
2026-01-17 15:31:44 +00:00
parent 319289cfac
commit eed8552282
13 changed files with 626 additions and 3 deletions
+69
View File
@@ -0,0 +1,69 @@
# Mode2Test
Complete OAuth2 authentication system with persistent token management.
## Structure
```
Mode2Test/
AuthAndToken/ # Everything related to auth and tokens
package.json
backend/
server.ts # Hono server
token-service.ts # Token storage & refresh
auth-routes.ts # Hono API endpoints
frontend/
index.html # Main app (requires auth)
signin.html # Microsoft OAuth2 login
auth-manager.ts # Frontend auth helpers
README.md # Component documentation
```
## Quick Start
1. Install dependencies:
```bash
cd Mode2Test/AuthAndToken
bun install
```
2. Set environment variables (in `secrets/.env`):
```
POCKETBASE_URL=https://pocketbase.ccllc.pro
```
3. Run:
```bash
bun run backend/server.ts
```
4. Open `http://localhost:3005` → redirects to signin.html if not authenticated
## How It Works
### Token Lifecycle
1. **Login**: User signs in via Microsoft OAuth2 → PB and Graph tokens acquired → stored in `tokens.json` → background refresh starts
2. **Refresh**: Every 5 minutes, both tokens auto-refresh silently
3. **Access**: Frontend/backend call `/api/auth/tokens` to get current state
4. **Logout**: User clicks logout → tokens cleared
### Startup Behavior
- If `tokens.json` exists with valid PB token: background refresh resumes immediately
- If no valid token: frontend redirects to `signin.html` to require login
- Tokens are **never deleted** until fresh ones are confirmed available
## API Endpoints
- `GET /api/auth/status` - Check auth status
- `GET /api/auth/tokens` - Get current tokens (requires auth)
- `POST /api/auth/login` - Store tokens after OAuth login
- `POST /api/auth/logout` - Clear all tokens
## Tokens
- **PocketBase Token**: Required. Refreshes automatically every 5 minutes.
- **Graph Token**: Optional. Refreshes automatically if available.
Both stay fresh at all times after initial login—no user intervention needed.
@@ -0,0 +1,82 @@
import { Hono } from 'hono';
import type { Context } from 'hono';
import { getTokens, saveTokens, clearTokens, hasValidPbToken } from './token-service';
// Authentication API routes
// Endpoints for frontend to check auth status, get tokens, logout
export function createAuthRoutes(): Hono {
const router = new Hono();
// PERMANENT: Check if user is authenticated
// Returns current auth status and which tokens are available
router.get('/status', async (c: Context) => {
const hasValid = await hasValidPbToken();
return c.json({
authenticated: hasValid,
pbTokenValid: hasValid,
message: hasValid ? 'User authenticated' : 'User not authenticated'
});
});
// PERMANENT: Get current tokens
// Frontend/backend can call this to get fresh token state
// Only returns tokens if PB token is valid (auth required)
router.get('/tokens', async (c: Context) => {
const hasValid = await hasValidPbToken();
if (!hasValid) {
return c.json({ error: 'Not authenticated' }, 401);
}
const tokens = await getTokens();
return c.json({
pbToken: tokens?.pbToken || null,
graphToken: tokens?.graphToken || null,
pbTokenExpiry: tokens?.pbTokenExpiry || null,
graphTokenExpiry: tokens?.graphTokenExpiry || null
});
});
// PERMANENT: Handle login callback from OAuth2
// Frontend sends back the PB auth response after successful login
// Stores both PB and Graph tokens
router.post('/login', async (c: Context) => {
try {
const body = await c.req.json() as any;
const pbToken = body.pbToken;
const graphToken = body.graphToken;
if (!pbToken) {
return c.json({ error: 'Missing pbToken' }, 400);
}
await saveTokens(pbToken, graphToken);
return c.json({
success: true,
message: 'Login successful',
authenticated: true
});
} catch (err) {
console.log('[AuthAndToken] Login error:', (err as Error)?.message);
return c.json({ error: 'Login failed' }, 500);
}
});
// PERMANENT: Logout - clear all tokens
// Called when user clicks logout
router.post('/logout', async (c: Context) => {
try {
await clearTokens();
return c.json({
success: true,
message: 'Logged out',
authenticated: false
});
} catch (err) {
console.log('[AuthAndToken] Logout error:', (err as Error)?.message);
return c.json({ error: 'Logout failed' }, 500);
}
});
return router;
}
+43
View File
@@ -0,0 +1,43 @@
import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import { join } from 'path';
import { initializeTokenService, startBackgroundRefresh } from './token-service';
import { createAuthRoutes } from './auth-routes';
// PERMANENT: Initialize token service and start background refresh
// Loads existing tokens from file if available
// Starts 5-minute background refresh cycle
initializeTokenService();
startBackgroundRefresh();
// Create Hono app
const app = new Hono();
// Mount auth routes
app.route('/api/auth', createAuthRoutes());
// Health check endpoint
app.get('/health', (c) => {
return c.text('OK');
});
// Version endpoint
app.get('/version', (c) => {
return c.json({ version: '1.0.0-beta1' });
});
// PERMANENT: Serve static frontend files
// Serves from frontend/ directory
const publicDir = join(import.meta.dir, '../frontend');
app.use('/*', serveStatic({ root: publicDir }));
// Start server on port 3005
const port = 3005;
console.log(`Mode2Test (AuthAndToken) running on http://localhost:${port}`);
console.log(`Auth API: /api/auth/*`);
console.log(`SignIn page: /signin.html`);
export default {
fetch: app.fetch,
port,
};
@@ -0,0 +1,144 @@
import { join } from 'path';
// Token storage and refresh management
// Stores both PocketBase and Graph tokens locally (tokens.json)
// Background refresh keeps both tokens fresh without user intervention
interface TokenData {
pbToken: string;
pbTokenExpiry: number;
graphToken?: string;
graphTokenExpiry?: number;
lastRefresh: number;
}
const tokensFilePath = join(import.meta.dir, '../../tokens.json');
// PERMANENT: Token lifecycle management
// - Tokens stored in tokens.json (persistent across restarts)
// - Never delete old token before new one acquired
// - Background refresh every 5 minutes
// - Startup: if PB token exists, resume refresh; else require login
export async function initializeTokenService(): Promise<void> {
// Load tokens from file if they exist
try {
const file = Bun.file(tokensFilePath);
if (await file.exists()) {
const content = await file.text();
const data = JSON.parse(content) as TokenData;
console.log('[AuthAndToken] Tokens loaded from file');
console.log('[AuthAndToken] PB token valid:', data.pbToken ? 'yes' : 'no');
console.log('[AuthAndToken] Graph token valid:', data.graphToken ? 'yes' : 'no');
} else {
console.log('[AuthAndToken] No tokens.json found - login required on startup');
}
} catch (err) {
console.log('[AuthAndToken] Token file init error (will create on first login):', (err as Error)?.message);
}
}
export function startBackgroundRefresh(): void {
// PERMANENT: Background token refresh
// Runs every 5 minutes, attempts to refresh both PB and Graph tokens
// Safe: never deletes old token before new one is confirmed
setInterval(async () => {
try {
await refreshTokens();
} catch (err) {
console.log('[AuthAndToken] Background refresh error:', (err as Error)?.message);
}
}, 5 * 60 * 1000); // 5 minutes
console.log('[AuthAndToken] Background refresh started (every 5 minutes)');
}
export async function saveTokens(pbToken: string, graphToken?: string): Promise<void> {
// PERMANENT: Token persistence
// Called after login or successful refresh
// Stores both tokens with expiry times
const data: TokenData = {
pbToken,
pbTokenExpiry: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days (PB default)
graphToken,
graphTokenExpiry: graphToken ? Date.now() + 60 * 60 * 1000 : undefined, // 1 hour (Graph default)
lastRefresh: Date.now()
};
await Bun.write(tokensFilePath, JSON.stringify(data, null, 2));
console.log('[AuthAndToken] Tokens saved');
}
export async function getTokens(): Promise<TokenData | null> {
try {
const file = Bun.file(tokensFilePath);
if (!(await file.exists())) return null;
const content = await file.text();
return JSON.parse(content) as TokenData;
} catch (err) {
console.log('[AuthAndToken] Error reading tokens:', (err as Error)?.message);
return null;
}
}
export async function clearTokens(): Promise<void> {
try {
const file = Bun.file(tokensFilePath);
if (await file.exists()) {
await Bun.write(tokensFilePath, '');
}
} catch (err) {
console.log('[AuthAndToken] Error clearing tokens:', (err as Error)?.message);
}
}
async function refreshTokens(): Promise<void> {
// PERMANENT: Auto-refresh logic for both tokens
// Called every 5 minutes by background job
// Fetches fresh tokens from PocketBase, attempts Graph token refresh
// Only updates file after new tokens confirmed valid
const tokens = await getTokens();
if (!tokens || !tokens.pbToken) {
console.log('[AuthAndToken] No active tokens to refresh');
return;
}
try {
// Refresh via PocketBase endpoint
const pbUrl = `${process.env.POCKETBASE_URL}/api/collections/users/auth-refresh`;
const response = await fetch(pbUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${tokens.pbToken}`
}
});
if (!response.ok) {
console.log('[AuthAndToken] Refresh failed:', response.status);
return;
}
const data = await response.json() as any;
const newPbToken = data?.token;
if (!newPbToken) {
console.log('[AuthAndToken] No new PB token in response');
return;
}
// Update tokens with new PB token
await saveTokens(newPbToken, tokens.graphToken);
console.log('[AuthAndToken] Tokens refreshed successfully');
} catch (err) {
console.log('[AuthAndToken] Refresh error:', (err as Error)?.message);
}
}
export async function hasValidPbToken(): Promise<boolean> {
const tokens = await getTokens();
return !!tokens?.pbToken && tokens.pbTokenExpiry > Date.now();
}
+35
View File
@@ -0,0 +1,35 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "mode2test",
"dependencies": {
"dotenv": "17.2.3",
"hono": "4.11.4",
"pocketbase": "0.26.5",
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "5.9.3",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mode2Test</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
</head>
<body class="font-sans bg-gray-50 min-h-screen">
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-800">Mode2Test</h1>
<p class="text-sm text-gray-600">Authenticated Application</p>
</div>
<div class="flex items-center gap-4">
<div id="userDisplay" class="text-right">
<p id="userName" class="text-sm font-medium text-gray-800"></p>
<p id="userEmail" class="text-xs text-gray-600"></p>
</div>
<button onclick="handleLogout()" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
Logout
</button>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-8">
<div class="bg-white p-8 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4 text-gray-800">Welcome to Mode2Test</h2>
<p class="text-gray-600 mb-6">Authenticated and ready to work.</p>
<div class="bg-gray-50 p-4 rounded border border-gray-200">
<h3 class="font-medium mb-2 text-gray-800">Token Status</h3>
<div id="tokenStatus" class="text-sm text-gray-600 space-y-2">
<p>Loading...</p>
</div>
</div>
</div>
</main>
<script>
const pb = new PocketBase('https://pocketbase.ccllc.pro');
async function checkAuth() {
if (!pb.authStore.isValid) {
// Try to refresh
try {
await pb.collection('users').authRefresh();
} catch {
window.location.href = '/signin.html';
return;
}
}
// Logged in - show app
document.getElementById('userName').textContent = pb.authStore.model?.display_name || pb.authStore.model?.email || 'User';
document.getElementById('userEmail').textContent = pb.authStore.model?.email || '';
const response = await fetch('/api/auth/tokens');
if (response.ok) {
const tokens = await response.json();
const pbValid = tokens.pbToken ? '✓ Valid' : '✗ Missing';
const graphValid = tokens.graphToken ? '✓ Valid' : '✗ Not acquired';
document.getElementById('tokenStatus').innerHTML = `
<div><p><strong>PocketBase Token:</strong> ${pbValid}</p></div>
<div><p><strong>Graph Token:</strong> ${graphValid}</p></div>
`;
}
}
function handleLogout() {
if (confirm('Logout?')) {
pb.authStore.clear();
fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/signin.html';
}
}
checkAuth();
</script>
</body>
</html>
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In - Mode2Test</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
</head>
<body class="font-sans bg-gradient-to-br from-blue-500 to-indigo-600 min-h-screen flex items-center justify-center">
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
<h1 class="text-gray-800 mb-2 text-3xl font-bold">Mode2Test</h1>
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
<button id="loginBtn" onclick="handleLogin()" class="w-full bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400">
Sign in with Microsoft
</button>
<div id="error" class="text-red-600 mt-4 hidden"></div>
</div>
<script>
const pb = new PocketBase('https://pocketbase.ccllc.pro');
async function handleLogin() {
const btn = document.getElementById('loginBtn');
btn.disabled = true;
btn.textContent = 'Signing in...';
document.getElementById('error').classList.add('hidden');
try {
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
// Log what we got for debugging
console.log('[Auth] OAuth response:', authData);
console.log('[Auth] Meta:', authData?.meta);
// Try to extract graph token from various possible locations
const graphToken =
authData?.meta?.accessToken ||
authData?.meta?.access_token ||
authData?.meta?.graphAccessToken ||
authData?.meta?.graph_token ||
authData?.meta?.graphToken ||
authData?.meta?.rawUser?.access_token ||
'';
console.log('[Auth] Graph token found:', !!graphToken);
// Send to backend to store
await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pbToken: authData.token, graphToken: graphToken || undefined })
});
// Redirect to app immediately
window.location.href = '/index.html';
} catch (err) {
document.getElementById('error').textContent = err.message || 'Login failed';
document.getElementById('error').classList.remove('hidden');
btn.disabled = false;
btn.textContent = 'Sign in with Microsoft';
}
}
// Check if already logged in
if (pb.authStore.isValid) {
window.location.href = '/index.html';
}
</script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
{
"name": "mode2test",
"version": "1.0.0-beta1",
"type": "module",
"scripts": {
"dev": "bun run backend/server.ts",
"start": "bun run backend/server.ts"
},
"dependencies": {
"hono": "4.11.4",
"dotenv": "17.2.3",
"pocketbase": "0.26.5"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "5.9.3"
}
}
-1
View File
@@ -1 +0,0 @@
Mode2Test placeholder - will be implemented next
+7
View File
@@ -0,0 +1,7 @@
{
"pbToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE4MDAxOTk4MTksImlkIjoiMWhiaWY5ZGJxbmc4Nmg5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.CqJgLgGfLOk4Wk9ro16MwtVjSMYG2oY4s9GV_tLBvvc",
"pbTokenExpiry": 1769268619806,
"graphToken": "eyJ0eXAiOiJKV1QiLCJub25jZSI6IlBQamVpUk5STGVWbkpNWVJSNHlzVENhMEtlZzJTeEhwOTFLbll6NUxYcGciLCJhbGciOiJSUzI1NiIsIng1dCI6IlBjWDk4R1g0MjBUMVg2c0JEa3poUW1xZ3dNVSIsImtpZCI6IlBjWDk4R1g0MjBUMVg2c0JEa3poUW1xZ3dNVSJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8zZmQ5N2VhNy1iMTI0LTQxZjEtODU1Zi01MmQ4YWMzYjE2YzcvIiwiaWF0IjoxNzY4NjYzNTE5LCJuYmYiOjE3Njg2NjM1MTksImV4cCI6MTc2ODY2ODg3NSwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsicDEiXSwiYWlvIjoiQVpRQWEvOGJBQUFBeTlyZWNsVzErcHErWE5hNXY0MkVrQUhaTEZwbGJ2SVJxZzFGWFprcWphbTR0eXk2dFdUTVFXZXNobGdJUFg2ZThMUXhoV2hrRE5MRDBuNE9mZW03a2QrcE1PM0ZPQTM5WmNFNFZjcWt1cWlTaUx1aFc0UDYyVkErOGV0ekZOOWFhcFdrMlR1T0ZQbVdpbDJEOEJ5QURaZmxFRTU5QkRtRVdsSTNvRkxZc3lvc2YwK1VtNGMyV2xsUTEwVjJQTFlLIiwiYW1yIjpbInB3ZCIsIm1mYSJdLCJhcHBfZGlzcGxheW5hbWUiOiJKb2JfSW5mb19TeW5jIiwiYXBwaWQiOiIzYzg0NmU3MS05NjA5LTQwZTEtYjQ1OC0wZWI4MDVlMjFiOWYiLCJhcHBpZGFjciI6IjEiLCJmYW1pbHlfbmFtZSI6IkV3aW5nIiwiZ2l2ZW5fbmFtZSI6IkFsZm9uc28iLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIxNzIuNTkuNzIuMjQ4IiwibmFtZSI6IkFsZm9uc28gRXdpbmciLCJvaWQiOiIxYTZlOWQxMC0xMzhhLTQzZTQtYTA5ZS0xZjUwNDYzMTQ4YzkiLCJwbGF0ZiI6IjMiLCJwdWlkIjoiMTAwMzIwMDUxRkVCOEFDMCIsInJoIjoiMS5BVFVBcDM3WlB5U3g4VUdGWDFMWXJEc1d4d01BQUFBQUFBQUF3QUFBQUFBQUFBQmxBVWsxQUEuIiwic2NwIjoiQ2hhbm5lbC5SZWFkQmFzaWMuQWxsIENoYW5uZWxNZW1iZXIuUmVhZC5BbGwgQ2hhbm5lbE1lbWJlci5SZWFkV3JpdGUuQWxsIENoYW5uZWxNZXNzYWdlLkVkaXQgQ2hhbm5lbE1lc3NhZ2UuUmVhZC5BbGwgQ2hhbm5lbE1lc3NhZ2UuUmVhZFdyaXRlIENoYW5uZWxNZXNzYWdlLlNlbmQgQ2hhbm5lbFNldHRpbmdzLlJlYWQuQWxsIENoYW5uZWxTZXR0aW5ncy5SZWFkV3JpdGUuQWxsIENoYXRNZXNzYWdlLlJlYWQgQ2hhdE1lc3NhZ2UuU2VuZCBEaXJlY3RvcnkuQWNjZXNzQXNVc2VyLkFsbCBEaXJlY3RvcnkuUmVhZC5BbGwgRGlyZWN0b3J5LlJlYWRXcml0ZS5BbGwgZW1haWwgRmlsZXMuUmVhZFdyaXRlLkFsbCBHcm91cC5SZWFkLkFsbCBHcm91cC5SZWFkV3JpdGUuQWxsIEdyb3VwLUNvbnZlcnNhdGlvbi5SZWFkLkFsbCBHcm91cC1Db252ZXJzYXRpb24uUmVhZFdyaXRlLkFsbCBHcm91cE1lbWJlci5SZWFkLkFsbCBHcm91cE1lbWJlci5SZWFkV3JpdGUuQWxsIEdyb3VwU2V0dGluZ3MuUmVhZC5BbGwgR3JvdXBTZXR0aW5ncy5SZWFkV3JpdGUuQWxsIHByb2ZpbGUgU2l0ZXMuUmVhZFdyaXRlLkFsbCBUYXNrcy5SZWFkIFRhc2tzLlJlYWQuU2hhcmVkIFRhc2tzLlJlYWRXcml0ZSBUYXNrcy5SZWFkV3JpdGUuU2hhcmVkIFRlYW1zQWN0aXZpdHkuUmVhZCBUZWFtU2V0dGluZ3MuUmVhZC5BbGwgVGVhbVNldHRpbmdzLlJlYWRXcml0ZS5BbGwgVGVhbXNQb2xpY3lVc2VyQXNzaWduLlJlYWRXcml0ZS5BbGwgVXNlci5SZWFkIG9wZW5pZCIsInNpZCI6IjAwMTAzNDhhLTdkYWItNzBlZi1mODdmLWYyNGJhM2M5OTY2YyIsInNpZ25pbl9zdGF0ZSI6WyJrbXNpIl0sInN1YiI6IkFzVnNHelNzVE5hVm5SYk1sQ1p5VnlrWTJYejRIcktEaUVsdXlFWll2U3ciLCJ0ZW5hbnRfcmVnaW9uX3Njb3BlIjoiTkEiLCJ0aWQiOiIzZmQ5N2VhNy1iMTI0LTQxZjEtODU1Zi01MmQ4YWMzYjE2YzciLCJ1bmlxdWVfbmFtZSI6ImFld2luZ0BjYXJkb3phLmNvbnN0cnVjdGlvbiIsInVwbiI6ImFld2luZ0BjYXJkb3phLmNvbnN0cnVjdGlvbiIsInV0aSI6IjhadmQ0dkhBODA2Wmx1dnpJYkhaQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbImZlOTMwYmU3LTVlNjItNDdkYi05MWFmLTk4YzNhNDlhMzhiMSIsIjYyZTkwMzk0LTY5ZjUtNDIzNy05MTkwLTAxMjE3NzE0NWUxMCIsIjI5MjMyY2RmLTkzMjMtNDJmZC1hZGUyLTFkMDk3YWYzZTRkZSIsIjY5MDkxMjQ2LTIwZTgtNGE1Ni1hYTRkLTA2NjA3NWIyYTdhOCIsImI3OWZiZjRkLTNlZjktNDY4OS04MTQzLTc2YjE5NGU4NTUwOSJdLCJ4bXNfYWNkIjoxNzYzNDA1ODY0LCJ4bXNfYWN0X2ZjdCI6IjkgMyIsInhtc19mdGQiOiJvV0dWXzgxcHlJQksxWVlOYTJ6ck1sYlJsSENobmg2T1JtZVUwY1Y2d0ZBQmRYTjNaWE4wTXkxa2MyMXoiLCJ4bXNfaWRyZWwiOiIxIDQiLCJ4bXNfc3QiOnsic3ViIjoiQUhmNlFEcUV0ZHZEeXptWjB5TDJ0ZmRJLUltWVJxRlRJUWFMczdiSnZQYyJ9LCJ4bXNfc3ViX2ZjdCI6IjMgMiIsInhtc190Y2R0IjoxNTU4NTQ2MDA3LCJ4bXNfdG50X2ZjdCI6IjMgMiJ9.Dxx8iCgvn4kSLmXSdSKOwCB188jCQsLd-w5XBqNLuxlZXfzp8_DH_XVvZysGcffDrKkquSJ6oYOh4IjMy1ZLBl6lTz44Q3UoWiNZwZChsfNpyfk7lgI9ZnrLJizNFi9hpKOS28_pncT1ZRWJY6pIhs-coLO4_08CqynT3Vq_6LISwm1-rBOWr5A1_cP1An2Rnuu_3PEW8hmFHmMTvbXyIW9xxsExiNlTqr6w5h-dBMcOuzhuqRiDMhKplR8PaZrTdD8TizXAfaCepTyZPPa3rTKEn8jqiKmerit3mcx1H5RCFIgmxygdoTJVxXbEMx1SUfDb2t_M6r_yVjpNXfyqgQ",
"graphTokenExpiry": 1768667419806,
"lastRefresh": 1768663819806
}
+1 -1
View File
@@ -1 +1 @@
Mode1Test Mode2Test
+9 -1
View File
@@ -80,9 +80,17 @@ async function startMode(mode: string): Promise<boolean> {
console.log(`[ModeSwitch] Starting ${mode} (${folderName}/) on port ${SHARED_PORT}...`); console.log(`[ModeSwitch] Starting ${mode} (${folderName}/) on port ${SHARED_PORT}...`);
// Determine server path based on mode
// Mode2Test uses AuthAndToken/backend/server.ts
// Mode1Test uses backend/server.ts
let serverPath = 'backend/server.ts';
if (mode === 'Mode2Test') {
serverPath = 'AuthAndToken/backend/server.ts';
}
// Start the mode's backend server // Start the mode's backend server
currentProcess = Bun.spawn({ currentProcess = Bun.spawn({
cmd: ['bun', 'run', 'backend/server.ts'], cmd: ['bun', 'run', serverPath],
cwd: modePath, cwd: modePath,
stdout: 'pipe', stdout: 'pipe',
stderr: 'pipe', stderr: 'pipe',
+61
View File
@@ -0,0 +1,61 @@
# Global Components Library
Reusable components for all modes and projects.
## Purpose
This folder contains battle-tested, production-ready components that are shared across Mode1Test, Mode2Test, and future modes. When a component proves solid, it gets promoted here for wider use.
## Structure
Each component lives in its own folder with:
- Component code (TypeScript)
- Styling (TailwindCSS utilities)
- Documentation (how to use, props, dependencies, adjustments)
- Examples (if complex)
Example structure:
```
components/
JobCard/
JobCard.ts # Component implementation
README.md # Usage documentation
```
## Using Components in a Mode
Import from the parent directory:
```typescript
import JobCard from '../../../components/JobCard/JobCard';
```
Or configure path aliases in your mode's `tsconfig.json`:
```json
{
"compilerOptions": {
"paths": {
"@components/*": ["../components/*"]
}
}
}
```
Then import as:
```typescript
import JobCard from '@components/JobCard/JobCard';
```
## Adding New Components
1. Create a folder with the component name
2. Write TypeScript with full type safety
3. Use only TailwindCSS for styling
4. Document: what it does, how to use, what props/env vars needed
5. Test in a mode thoroughly
6. When proven, commit with clear description
## Components
(None yet—first proven component will be added here)