fixed login loop
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user