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}...`);
|
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',
|
||||||
|
|||||||
@@ -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