Add Mode1 auth testing environment with token comparison verification
- Created Mode1 as self-contained test environment - Added auth-module copy with self-contained configuration - Implemented backend server with Graph token and PocketBase validation endpoints - Added frontend test dashboard with token comparison functionality - Fixed TypeScript compatibility issues (replaced import.meta.dir with __dirname) - Implemented actual PB token validation in comparison endpoint (no placeholders) - All endpoints tested and working with real tokens
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import { AuthConfig, AuthState, AuthCallbacks } from './types';
|
||||
|
||||
/**
|
||||
* PocketBase OAuth2 Frontend Module
|
||||
* Handles user authentication and token management
|
||||
*/
|
||||
export class PocketBaseAuth {
|
||||
private pb: PocketBase;
|
||||
private config: Required<AuthConfig>;
|
||||
private callbacks: AuthCallbacks;
|
||||
private state: AuthState;
|
||||
|
||||
constructor(config: AuthConfig, callbacks?: Partial<AuthCallbacks>) {
|
||||
this.config = {
|
||||
pbUrl: config.pbUrl!,
|
||||
collection: config.collection || 'Users',
|
||||
provider: config.provider || 'microsoft',
|
||||
loginContainerId: config.loginContainerId || 'loginContainer',
|
||||
userDisplayNameId: config.userDisplayNameId || 'userDisplayName',
|
||||
userEmailId: config.userEmailId || 'userEmailValue',
|
||||
loginBtnId: config.loginBtnId || 'loginBtn',
|
||||
loginErrorId: config.loginErrorId || 'loginError',
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
onAuthSuccess: callbacks?.onAuthSuccess || (() => {}),
|
||||
onAuthFailure: callbacks?.onAuthFailure || (() => {}),
|
||||
onTokenUpdate: callbacks?.onTokenUpdate || (() => {}),
|
||||
onUiUpdate: callbacks?.onUiUpdate || (() => {}),
|
||||
};
|
||||
|
||||
this.pb = new PocketBase(this.config.pbUrl);
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize auth module
|
||||
*/
|
||||
private init(): void {
|
||||
this.updateAuthUI();
|
||||
if (this.pb.authStore.isValid && this.pb.authStore.token) {
|
||||
this.ensureUserLogged();
|
||||
}
|
||||
this.setupLoginButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup login button click handler
|
||||
*/
|
||||
private setupLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId);
|
||||
if (!loginBtn) {
|
||||
console.warn(`Login button with id "${this.config.loginBtnId}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
loginBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.handleLogin();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login button click
|
||||
*/
|
||||
private async handleLogin(): Promise<void> {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement;
|
||||
|
||||
if (!loginBtn || !loginError) return;
|
||||
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Checking session...';
|
||||
loginError.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// Try to use existing token first
|
||||
const hadToken = await this.ensureUserLogged();
|
||||
if (hadToken) {
|
||||
this.resetLoginButton();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise perform OAuth
|
||||
loginBtn.textContent = 'Logging in...';
|
||||
const authData = await this.pb.collection(this.config.collection).authWithOAuth2({
|
||||
provider: this.config.provider,
|
||||
});
|
||||
|
||||
this.updateState(authData);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onAuthSuccess?.(this.state);
|
||||
|
||||
console.log('✓ Logged in with', this.config.provider);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Login failed:', message);
|
||||
loginError.textContent = `Login failed: ${message}`;
|
||||
loginError.classList.remove('hidden');
|
||||
this.callbacks.onAuthFailure?.(error);
|
||||
} finally {
|
||||
this.resetLoginButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user is logged in (check existing token)
|
||||
*/
|
||||
async ensureUserLogged(): Promise<boolean> {
|
||||
if (!this.pb.authStore.isValid || !this.pb.authStore.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refresh = await this.pb.collection(this.config.collection).authRefresh();
|
||||
this.updateState(refresh);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onTokenUpdate?.(this.state);
|
||||
console.log('✓ Token refreshed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Existing token invalid, clearing auth');
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update internal auth state
|
||||
*/
|
||||
private updateState(data: any): void {
|
||||
if (!data) {
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const record = data.record || this.pb.authStore.record || this.pb.authStore.model;
|
||||
const meta = data.meta || {};
|
||||
const model = this.pb.authStore.model;
|
||||
|
||||
this.state = {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
id: record?.id || model?.id || 'unknown',
|
||||
name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User',
|
||||
email: record?.email || model?.email || meta?.email || '(no email)',
|
||||
},
|
||||
token: this.pb.authStore.token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI based on auth state
|
||||
*/
|
||||
updateAuthUI(): void {
|
||||
const loginContainer = document.getElementById(this.config.loginContainerId);
|
||||
const userDisplayName = document.getElementById(this.config.userDisplayNameId);
|
||||
const userEmail = document.getElementById(this.config.userEmailId);
|
||||
|
||||
if (!loginContainer) return;
|
||||
|
||||
if (this.pb.authStore.isValid) {
|
||||
loginContainer.classList.add('hidden');
|
||||
if (this.state.user) {
|
||||
if (userDisplayName) userDisplayName.textContent = this.state.user.name;
|
||||
if (userEmail) userEmail.textContent = this.state.user.email;
|
||||
}
|
||||
} else {
|
||||
loginContainer.classList.remove('hidden');
|
||||
const loginError = document.getElementById(this.config.loginErrorId);
|
||||
if (loginError) loginError.classList.add('hidden');
|
||||
}
|
||||
|
||||
this.callbacks.onUiUpdate?.(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check token status (for verification/display)
|
||||
*/
|
||||
async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> {
|
||||
const pbToken = !!this.pb.authStore.token;
|
||||
return { pbToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current auth state
|
||||
*/
|
||||
getAuthState(): AuthState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance (for direct usage if needed)
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout(): void {
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
console.log('✓ Logged out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset login button to initial state
|
||||
*/
|
||||
private resetLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
if (loginBtn) {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Login with Microsoft';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick init function - fetches config from backend
|
||||
*/
|
||||
export async function initPocketBaseAuth(
|
||||
backendUrl: string,
|
||||
callbacks?: Partial<AuthCallbacks>
|
||||
): Promise<PocketBaseAuth> {
|
||||
// Fetch frontend config from backend
|
||||
const response = await fetch(`${backendUrl}/api/auth/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch auth configuration from backend');
|
||||
}
|
||||
const config = await response.json();
|
||||
|
||||
const auth = new PocketBaseAuth(config, callbacks);
|
||||
return auth;
|
||||
}
|
||||
Reference in New Issue
Block a user