- Create centralized logger module (src/core/logger.ts) - Replace all debugLog() calls with log.debug() - Replace console.log() with log.info() - Replace console.warn() with log.warn() - Replace console.error() with log.error() - Delete deprecated src/core/debug.ts - Configure log levels: debug for dev, warn for production - Add localStorage override for production debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
204 lines
6.9 KiB
TypeScript
204 lines
6.9 KiB
TypeScript
import { createAuth0Client, Auth0Client, User } from '@auth0/auth0-spa-js';
|
|
import log from '../core/logger';
|
|
|
|
/**
|
|
* Singleton service for managing Auth0 authentication
|
|
* Handles login, logout, token management, and user state
|
|
*/
|
|
export class AuthService {
|
|
private static _instance: AuthService;
|
|
private _client: Auth0Client | null = null;
|
|
private _user: User | null = null;
|
|
|
|
private constructor() {}
|
|
|
|
/**
|
|
* Get the singleton instance of AuthService
|
|
*/
|
|
public static getInstance(): AuthService {
|
|
if (!AuthService._instance) {
|
|
AuthService._instance = new AuthService();
|
|
}
|
|
return AuthService._instance;
|
|
}
|
|
|
|
/**
|
|
* Initialize the Auth0 client and handle redirect callback
|
|
* Call this early in the application lifecycle
|
|
*/
|
|
public async initialize(): Promise<void> {
|
|
log.info('[AuthService] ========== INITIALIZE CALLED ==========');
|
|
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
|
|
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
|
|
|
|
log.info('[AuthService] Config:', {
|
|
domain,
|
|
clientId: clientId ? clientId.substring(0, 10) + '...' : 'missing',
|
|
redirectUri: window.location.origin
|
|
});
|
|
|
|
if (!domain || !clientId || domain.trim() === '') {
|
|
log.warn('[AuthService] Auth0 not configured - authentication features will be disabled');
|
|
return;
|
|
}
|
|
|
|
log.info('[AuthService] Creating Auth0 client...');
|
|
const audience = import.meta.env.VITE_AUTH0_AUDIENCE;
|
|
this._client = await createAuth0Client({
|
|
domain,
|
|
clientId,
|
|
authorizationParams: {
|
|
redirect_uri: window.location.origin,
|
|
audience: audience || undefined
|
|
},
|
|
cacheLocation: 'localstorage', // Persist tokens across page reloads
|
|
useRefreshTokens: true // Enable silent token refresh
|
|
});
|
|
log.info('[AuthService] Auth0 client created successfully');
|
|
|
|
// Handle redirect callback after login
|
|
const hasCallback = window.location.search.includes('code=') ||
|
|
window.location.search.includes('state=');
|
|
log.info('[AuthService] Checking for Auth0 callback:', hasCallback);
|
|
log.info('[AuthService] Current URL:', window.location.href);
|
|
|
|
if (hasCallback) {
|
|
log.info('[AuthService] ========== PROCESSING AUTH0 CALLBACK ==========');
|
|
try {
|
|
const result = await this._client.handleRedirectCallback();
|
|
log.info('[AuthService] Callback handled successfully:', result);
|
|
// Clean up the URL after handling callback
|
|
window.history.replaceState({}, document.title, '/');
|
|
log.info('[AuthService] URL cleaned, redirected to home');
|
|
} catch (error) {
|
|
log.error('[AuthService] !!!!! CALLBACK ERROR !!!!!', error);
|
|
log.error('[AuthService] Error details:', error?.message, error?.stack);
|
|
}
|
|
}
|
|
|
|
// Check if user is authenticated and load user info
|
|
log.info('[AuthService] Checking authentication status...');
|
|
const isAuth = await this._client.isAuthenticated();
|
|
log.info('[AuthService] Is authenticated:', isAuth);
|
|
|
|
if (isAuth) {
|
|
log.info('[AuthService] Loading user info...');
|
|
this._user = await this._client.getUser() ?? null;
|
|
log.info('[AuthService] User loaded:', {
|
|
name: this._user?.name,
|
|
email: this._user?.email,
|
|
sub: this._user?.sub
|
|
});
|
|
} else {
|
|
log.info('[AuthService] User not authenticated');
|
|
}
|
|
|
|
log.info('[AuthService] ========== INITIALIZATION COMPLETE ==========');
|
|
}
|
|
|
|
/**
|
|
* Redirect to Auth0 login page
|
|
*/
|
|
public async login(): Promise<void> {
|
|
log.info('[AuthService] ========== LOGIN CALLED ==========');
|
|
if (!this._client) {
|
|
log.error('[AuthService] !!!!! CLIENT NOT INITIALIZED !!!!!');
|
|
throw new Error('Auth client not initialized. Call initialize() first.');
|
|
}
|
|
log.info('[AuthService] Redirecting to Auth0 login...');
|
|
await this._client.loginWithRedirect();
|
|
}
|
|
|
|
/**
|
|
* Log out the current user and redirect to home
|
|
*/
|
|
public async logout(): Promise<void> {
|
|
log.info('[AuthService] ========== LOGOUT CALLED ==========');
|
|
if (!this._client) {
|
|
log.error('[AuthService] !!!!! CLIENT NOT INITIALIZED !!!!!');
|
|
throw new Error('Auth client not initialized. Call initialize() first.');
|
|
}
|
|
this._user = null;
|
|
log.info('[AuthService] Logging out and redirecting to:', window.location.origin);
|
|
await this._client.logout({
|
|
logoutParams: {
|
|
returnTo: window.location.origin
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the user is currently authenticated
|
|
*/
|
|
public async isAuthenticated(): Promise<boolean> {
|
|
if (!this._client) return false;
|
|
return await this._client.isAuthenticated();
|
|
}
|
|
|
|
/**
|
|
* Get the current authenticated user's profile
|
|
*/
|
|
public getUser(): User | null {
|
|
return this._user;
|
|
}
|
|
|
|
/**
|
|
* Get an access token for making authenticated API calls
|
|
* Returns undefined if not authenticated
|
|
*/
|
|
public async getAccessToken(): Promise<string | undefined> {
|
|
if (!this._client) return undefined;
|
|
try {
|
|
return await this._client.getTokenSilently();
|
|
} catch (error) {
|
|
log.error('Error getting access token:', error);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user logged in via Facebook
|
|
* Auth0 stores the identity provider in the user's sub claim
|
|
*/
|
|
public isAuthenticatedWithFacebook(): boolean {
|
|
if (!this._user) return false;
|
|
|
|
// Check if user authenticated via Facebook
|
|
// Auth0 sub format: "facebook|{facebook-user-id}" for Facebook logins
|
|
const sub = this._user.sub || '';
|
|
return sub.startsWith('facebook|');
|
|
}
|
|
|
|
/**
|
|
* Get the authentication provider (facebook, google, auth0, etc.)
|
|
*/
|
|
public getAuthProvider(): string | null {
|
|
if (!this._user) return null;
|
|
|
|
const sub = this._user.sub || '';
|
|
const parts = sub.split('|');
|
|
|
|
if (parts.length >= 2) {
|
|
return parts[0]; // Returns 'facebook', 'google', 'auth0', etc.
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get user's Facebook ID if authenticated via Facebook
|
|
*/
|
|
public getFacebookUserId(): string | null {
|
|
if (!this.isAuthenticatedWithFacebook()) return null;
|
|
|
|
const sub = this._user?.sub || '';
|
|
const parts = sub.split('|');
|
|
|
|
if (parts.length >= 2) {
|
|
return parts[1]; // Returns the Facebook user ID
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|