Mode2Test/AuthAndToken: assume tokens always fresh with background refresh; fix jobs API to use correct collection; align Home UI to Mode1 (exact fields, styling, strict Active dot); add full-field IndexedDB cache w/ pagination; remove progress bar; dev TLS bypass; rebuild bundles

This commit is contained in:
2026-01-17 20:20:24 +00:00
parent eed8552282
commit d77fa4b817
21 changed files with 5964 additions and 205 deletions
+150 -118
View File
@@ -1,8 +1,20 @@
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
// PERMANENT: Token Service Module
// Singleton module for centralized token acquisition, storage, and refresh
// Available to any part of the application via: import { tokenService } from './token-service.ts'
//
// Token Lifecycle:
// - 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
//
// Usage:
// const tokens = await tokenService.getTokens();
// await tokenService.saveTokens(pbToken, graphToken);
// await tokenService.initialize();
// tokenService.startBackgroundRefresh();
interface TokenData {
pbToken: string;
@@ -14,131 +26,151 @@ interface TokenData {
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
class TokenService {
private backgroundStarted = false;
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 () => {
async initialize(): Promise<void> {
// Load tokens from file if they exist
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}`
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);
}
}
startBackgroundRefresh(): void {
if (this.backgroundStarted) return;
this.backgroundStarted = true;
// 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 this.refreshTokens();
} catch (err) {
console.log('[AuthAndToken] Background refresh error:', (err as Error)?.message);
}
}, 5 * 60 * 1000); // 5 minutes
// Kick off an immediate refresh attempt so we start from a fresh token without waiting
this.refreshTokens().catch((err) => {
console.log('[AuthAndToken] Immediate refresh error:', (err as Error)?.message);
});
if (!response.ok) {
console.log('[AuthAndToken] Refresh failed:', response.status);
console.log('[AuthAndToken] Background refresh started (every 5 minutes + immediate)');
}
async 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');
}
async 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);
try {
const fs = await import('fs/promises');
await fs.unlink(tokensFilePath);
console.log('[AuthAndToken] Corrupt tokens file removed');
} catch (innerErr) {
console.log('[AuthAndToken] Failed to remove corrupt tokens file:', (innerErr as Error)?.message);
}
return null;
}
}
async clearTokens(): Promise<void> {
try {
const file = Bun.file(tokensFilePath);
if (await file.exists()) {
const fs = await import('fs/promises');
await fs.unlink(tokensFilePath);
}
} catch (err) {
console.log('[AuthAndToken] Error clearing tokens:', (err as Error)?.message);
}
}
async hasValidPbToken(): Promise<boolean> {
// Simplified: treat presence of PB token as valid; no expiry enforcement
const tokens = await this.getTokens();
return !!tokens?.pbToken;
}
private async 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 this.getTokens();
if (!tokens || !tokens.pbToken) {
console.log('[AuthAndToken] No active tokens to refresh');
return;
}
const data = await response.json() as any;
const newPbToken = data?.token;
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}`
},
// @ts-ignore Bun TLS option
tls: { rejectUnauthorized: false }
});
if (!newPbToken) {
console.log('[AuthAndToken] No new PB token in response');
return;
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 this.saveTokens(newPbToken, tokens.graphToken);
console.log('[AuthAndToken] Tokens refreshed successfully');
} catch (err) {
console.log('[AuthAndToken] Refresh error:', (err as Error)?.message);
}
// 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();
}
// PERMANENT: Singleton export
// Import as: import { tokenService } from './token-service.ts'
export const tokenService = new TokenService();