Mode2Test AuthAndToken component complete: login/logout working, token persistence, background refresh active, tested and verified
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 +0,0 @@
|
||||
Mode2Test placeholder - will be implemented next
|
||||
@@ -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 @@
|
||||
Mode1Test
|
||||
Mode2Test
|
||||
@@ -80,9 +80,17 @@ async function startMode(mode: string): Promise<boolean> {
|
||||
|
||||
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
|
||||
currentProcess = Bun.spawn({
|
||||
cmd: ['bun', 'run', 'backend/server.ts'],
|
||||
cmd: ['bun', 'run', serverPath],
|
||||
cwd: modePath,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user