145 lines
4.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
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();
|
|
}
|