Files
Job-Info/NewApproach/auth/job-info/flow.ts
T
2026-01-10 19:23:47 +00:00

295 lines
8.3 KiB
TypeScript

/**
* Job-Info Auth Flow - Standalone
*
* Complete, self-contained authentication for Job-Info app
* - Check localStorage for active tokens on load
* - Prompt for login if tokens missing/inactive
* - Get user display name from PocketBase metadata
* - Refresh all tokens to keep them fresh
* - Only replace pb-user when we have a new token
* - Graph token is optional after first login
* - Do not prompt again unless pb-user is missing/inactive
*/
interface TokenMetadata {
value: string;
displayName: string;
lastFourDigits: string;
expiresAt: number;
acquiredAt: number;
}
interface JobInfoAuthState {
'pb-user'?: TokenMetadata;
'graph-user'?: TokenMetadata;
userDisplayName: string;
}
class JobInfoAuthFlow {
private state: JobInfoAuthState = {
userDisplayName: 'Unknown'
};
private pb: any = null;
private initialized = false;
/**
* Initialize and check token status
*/
async init(): Promise<void> {
if (this.initialized) return;
console.log('[JobInfoAuth] Starting init');
// Initialize PocketBase
const PocketBase = (window as any).PocketBase;
if (!PocketBase) {
throw new Error('PocketBase SDK not loaded. Include: <script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>');
}
this.pb = new PocketBase('http://localhost:8090');
console.log('[JobInfoAuth] PocketBase initialized');
// Load persisted tokens from localStorage
this.loadPersistedTokens();
// Check token status
const pbUserActive = this.isTokenActive('pb-user');
const graphUserActive = this.isTokenActive('graph-user');
console.log('[JobInfoAuth] Token status - pb-user:', pbUserActive, 'graph-user:', graphUserActive);
// If pb-user is missing or inactive: prompt for login
if (!pbUserActive) {
console.log('[JobInfoAuth] pb-user token missing or inactive, prompting login');
await this.promptLogin();
} else {
console.log('[JobInfoAuth] pb-user token is active, loading display name');
// Get display name from state or try to refresh
if (!this.state.userDisplayName || this.state.userDisplayName === 'Unknown') {
const userInfo = this.getUserInfo();
this.state.userDisplayName = userInfo.displayName || 'Unknown';
}
}
// Refresh all tokens to keep them fresh
await this.refreshAllTokens();
this.initialized = true;
console.log('[JobInfoAuth] Initialized. State:', this.state);
}
/**
* Prompt for login via PocketBase OAuth
*/
private async promptLogin(): Promise<void> {
console.log('[JobInfoAuth] Prompting for login with OAuth');
try {
// Authenticate with PocketBase OAuth (Microsoft provider)
const authData = await this.pb.collection('users').authWithOAuth2({
provider: 'microsoft'
});
console.log('[JobInfoAuth] OAuth successful, authData:', authData);
// Extract tokens
const pbUserToken = authData.token;
const graphUserToken = this.extractGraphToken(authData.meta || {});
if (!pbUserToken) {
throw new Error('No pb-user token in OAuth response');
}
// Get user display name
const userInfo = this.getUserInfo();
this.state.userDisplayName = userInfo.displayName || 'Unknown';
// Store tokens
this.storeToken('pb-user', pbUserToken);
if (graphUserToken) {
this.storeToken('graph-user', graphUserToken);
}
console.log('[JobInfoAuth] Login successful. User:', this.state.userDisplayName);
} catch (err) {
console.error('[JobInfoAuth] Login failed:', err);
throw err;
}
}
/**
* Extract Graph token from OAuth metadata
*/
private extractGraphToken(meta: any): string | null {
return (
meta?.graphAccessToken ||
meta?.graph_token ||
meta?.graphToken ||
meta?.accessToken ||
meta?.access_token ||
meta?.token ||
meta?.rawToken ||
meta?.authData?.access_token ||
null
);
}
/**
* Refresh all tokens to keep them fresh
*/
private async refreshAllTokens(): Promise<void> {
console.log('[JobInfoAuth] Refreshing all tokens');
try {
// Refresh pb-user if we have an active session
if (this.pb.authStore.isValid) {
console.log('[JobInfoAuth] PocketBase session is valid, refreshing');
const authData = await this.pb.collection('users').authRefresh();
const newPbToken = authData.token;
const currentPbToken = this.state['pb-user']?.value;
// Only replace pb-user if we got a new token
if (newPbToken && newPbToken !== currentPbToken) {
console.log('[JobInfoAuth] pb-user token refreshed (new token)');
this.storeToken('pb-user', newPbToken);
} else if (newPbToken) {
console.log('[JobInfoAuth] pb-user token still fresh, keeping existing');
}
// Try to get updated graph token
const graphToken = this.extractGraphToken(authData.meta || {});
if (graphToken && graphToken !== this.state['graph-user']?.value) {
console.log('[JobInfoAuth] graph-user token refreshed');
this.storeToken('graph-user', graphToken);
}
}
} catch (err) {
console.error('[JobInfoAuth] Token refresh error:', err);
// Don't throw - token refresh is best-effort
}
}
/**
* Store token with metadata
*/
private storeToken(type: 'pb-user' | 'graph-user', token: string): void {
const lastFour = token.substring(token.length - 4);
const metadata: TokenMetadata = {
value: token,
displayName: this.state.userDisplayName,
lastFourDigits: lastFour,
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // Assume 7 days
acquiredAt: Date.now()
};
this.state[type] = metadata;
// Persist to localStorage
const key = `auth:${type}`;
localStorage.setItem(key, JSON.stringify(metadata));
console.log(`[JobInfoAuth] Token stored: ${type} (last 4: ${lastFour})`);
}
/**
* Load tokens from localStorage
*/
private loadPersistedTokens(): void {
try {
const pbKey = 'auth:pb-user';
const graphKey = 'auth:graph-user';
const pbData = localStorage.getItem(pbKey);
const graphData = localStorage.getItem(graphKey);
if (pbData) {
this.state['pb-user'] = JSON.parse(pbData);
this.state.userDisplayName = this.state['pb-user'].displayName || 'Unknown';
console.log('[JobInfoAuth] Loaded pb-user from localStorage');
}
if (graphData) {
this.state['graph-user'] = JSON.parse(graphData);
console.log('[JobInfoAuth] Loaded graph-user from localStorage');
}
} catch (err) {
console.error('[JobInfoAuth] Error loading persisted tokens:', err);
}
}
/**
* Check if a token is active (exists and not expired)
*/
private isTokenActive(type: 'pb-user' | 'graph-user'): boolean {
const token = this.state[type];
if (!token || !token.value) {
return false;
}
// Check if expired
if (token.expiresAt && token.expiresAt < Date.now()) {
console.log(`[JobInfoAuth] Token expired: ${type}`);
return false;
}
return true;
}
/**
* Get user info from PocketBase
*/
private getUserInfo(): { displayName: string; email: string } {
const model = this.pb?.authStore?.model;
if (!model) {
return { displayName: 'Unknown', email: '' };
}
return {
displayName: model.display_name || model.name || model.email || model.username || 'Unknown',
email: model.email || model.username || ''
};
}
/**
* Get current state (for UI)
*/
getState(): JobInfoAuthState {
return { ...this.state };
}
/**
* Get specific token value
*/
getToken(type: 'pb-user' | 'graph-user'): string | undefined {
return this.state[type]?.value;
}
/**
* Get user display name
*/
getUserDisplayName(): string {
return this.state.userDisplayName;
}
/**
* Logout
*/
logout(): void {
console.log('[JobInfoAuth] Logging out');
this.pb?.authStore?.clear?.();
localStorage.removeItem('auth:pb-user');
localStorage.removeItem('auth:graph-user');
this.state = { userDisplayName: 'Unknown' };
this.initialized = false;
}
}
const JobInfoAuth = new JobInfoAuthFlow();
// Expose globally
if (typeof window !== 'undefined') {
window.JobInfoAuth = JobInfoAuth;
}
export default JobInfoAuth;