295 lines
8.3 KiB
TypeScript
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;
|