7d8fd9f251
- Created Mode2Svelte as a copy of Mode1Svelte - Updated branding (title, description) to distinguish from Mode1Svelte - Added Mode2Svelte support to orchestrator - Configured orchestrator to spawn Mode2Svelte with Node.js adapter - Set Mode2Svelte as active mode in activeMode.txt - Built production bundle for Mode2Svelte - Both Mode1Svelte and Mode2Svelte now managed by orchestrator on port 3005 - Orchestrator controls which mode runs via activeMode.txt configuration
251 lines
6.9 KiB
TypeScript
251 lines
6.9 KiB
TypeScript
import PocketBase from 'pocketbase';
|
|
import type { 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;
|
|
}
|