UNPKG

@profullstack/auth-system

Version:

Flexible authentication system with user registration, login/logout, password reset, and session management

650 lines (565 loc) 18.9 kB
/** * Auth Client for Browser Integration * * This client provides a browser-friendly wrapper around the auth-system module, * with localStorage integration for token storage and event handling. */ import { createAuthSystem, SupabaseAdapter } from '../index.js'; export class AuthClient { /** * Create a new AuthClient * @param {Object} options - Configuration options * @param {string} options.supabaseUrl - Supabase project URL * @param {string} options.supabaseKey - Supabase API key * @param {string} options.jwtSecret - Secret for signing JWT tokens * @param {Function} options.onAuthChanged - Callback for auth state changes * @param {Function} options.sendEmail - Function to send emails * @param {Object} options.subscriptionApi - API for subscription management */ constructor(options) { this.options = options; // Create Supabase adapter this.adapter = new SupabaseAdapter({ supabaseUrl: options.supabaseUrl, supabaseKey: options.supabaseKey, tableName: options.tableName || 'users', tokensTableName: options.tokensTableName || 'invalidated_tokens' }); // Create auth system this.auth = createAuthSystem({ adapter: this.adapter, tokenOptions: { secret: options.jwtSecret, accessTokenExpiry: options.accessTokenExpiry || 3600, // 1 hour refreshTokenExpiry: options.refreshTokenExpiry || 604800 // 7 days }, passwordOptions: { minLength: options.minPasswordLength || 8, requireUppercase: options.requireUppercase !== false, requireLowercase: options.requireLowercase !== false, requireNumbers: options.requireNumbers !== false, requireSpecialChars: options.requireSpecialChars || false }, emailOptions: { sendEmail: options.sendEmail || this._defaultSendEmail.bind(this), fromEmail: options.fromEmail || 'noreply@example.com', resetPasswordTemplate: options.resetPasswordTemplate || { subject: 'Reset Your Password', text: 'Click the link to reset your password: {token}', html: '<p>Click the link to reset your password: <a href="{token}">{token}</a></p>' }, verificationTemplate: options.verificationTemplate || { subject: 'Verify Your Email', text: 'Click the link to verify your email: {token}', html: '<p>Click the link to verify your email: <a href="{token}">{token}</a></p>' } } }); // Subscription API integration this.subscriptionApi = options.subscriptionApi; // Set up auth state change handler this.onAuthChanged = options.onAuthChanged || (() => {}); // Initialize from localStorage this._initFromLocalStorage(); } /** * Initialize from localStorage * @private */ _initFromLocalStorage() { // Check for existing token this.accessToken = localStorage.getItem('jwt_token'); this.refreshToken = localStorage.getItem('refresh_token'); this.user = JSON.parse(localStorage.getItem('user') || 'null'); // If we have a token, validate it if (this.accessToken) { this.validateToken(this.accessToken) .then(user => { if (user) { this.user = user; this._saveUserToLocalStorage(user); } else { // Token is invalid, try to refresh if (this.refreshToken) { this.refreshToken(this.refreshToken) .catch(() => { // If refresh fails, clear auth state this._clearAuthState(); }); } else { // No refresh token, clear auth state this._clearAuthState(); } } }) .catch(() => { // Error validating token, clear auth state this._clearAuthState(); }); } } /** * Default send email function (console log only) * @private * @param {Object} emailData - Email data */ async _defaultSendEmail(emailData) { console.log('Sending email:', emailData); // In a real implementation, this would send an email } /** * Save tokens to localStorage * @private * @param {Object} tokens - Tokens object */ _saveTokensToLocalStorage(tokens) { if (tokens.accessToken) { localStorage.setItem('jwt_token', tokens.accessToken); this.accessToken = tokens.accessToken; } if (tokens.refreshToken) { localStorage.setItem('refresh_token', tokens.refreshToken); this.refreshToken = tokens.refreshToken; } } /** * Save user to localStorage * @private * @param {Object} user - User object */ _saveUserToLocalStorage(user) { // Create a sanitized user object without sensitive data const sanitizedUser = { id: user.id, email: user.email, username: user.email ? user.email.split('@')[0] : null, profile: user.profile || {}, emailVerified: user.emailVerified, createdAt: user.createdAt, updatedAt: user.updatedAt, lastLoginAt: user.lastLoginAt, subscription: user.subscription || { status: 'unknown' } }; localStorage.setItem('user', JSON.stringify(sanitizedUser)); localStorage.setItem('username', user.email); this.user = sanitizedUser; } /** * Clear auth state * @private */ _clearAuthState() { localStorage.removeItem('jwt_token'); localStorage.removeItem('refresh_token'); localStorage.removeItem('user'); localStorage.removeItem('username'); localStorage.removeItem('subscription_data'); this.accessToken = null; this.refreshToken = null; this.user = null; // Trigger auth changed event this._triggerAuthChanged(); } /** * Trigger auth changed event * @private */ _triggerAuthChanged() { // Call the onAuthChanged callback this.onAuthChanged(this.isAuthenticated(), this.user); // Dispatch a custom event window.dispatchEvent(new CustomEvent('auth-changed', { detail: { authenticated: this.isAuthenticated(), user: this.user } })); } /** * Check if user is authenticated * @returns {boolean} - Whether the user is authenticated */ isAuthenticated() { return !!this.accessToken && !!this.user; } /** * Register a new user * @param {Object} userData - User registration data * @param {string} userData.email - User email * @param {string} userData.password - User password * @param {Object} userData.profile - User profile data * @param {boolean} userData.autoVerify - Auto-verify email (default: false) * @returns {Promise<Object>} - Registration result */ async register(userData) { try { // Register user with auth system const result = await this.auth.register(userData); // If auto-verified, save tokens and user data if (result.tokens) { this._saveTokensToLocalStorage(result.tokens); this._saveUserToLocalStorage(result.user); // Trigger auth changed event this._triggerAuthChanged(); } return result; } catch (error) { console.error('Registration error:', error); throw error; } } /** * Login a user * @param {Object} credentials - Login credentials * @param {string} credentials.email - User email * @param {string} credentials.password - User password * @returns {Promise<Object>} - Login result */ async login(credentials) { try { // Login user with auth system const result = await this.auth.login(credentials); // Save tokens and user data this._saveTokensToLocalStorage(result.tokens); this._saveUserToLocalStorage(result.user); // Check subscription status if subscription API is provided if (this.subscriptionApi && typeof this.subscriptionApi.checkSubscriptionStatus === 'function') { try { const subscriptionStatus = await this.subscriptionApi.checkSubscriptionStatus(credentials.email); // Update user with subscription data if (subscriptionStatus) { const updatedUser = { ...this.user, subscription: { plan: subscriptionStatus.plan || 'monthly', status: subscriptionStatus.status || 'active', expiresAt: subscriptionStatus.expires_at || null } }; this._saveUserToLocalStorage(updatedUser); localStorage.setItem('subscription_data', JSON.stringify(subscriptionStatus)); } } catch (subscriptionError) { console.warn('Error checking subscription status:', subscriptionError); // Continue even if subscription check fails } } // Trigger auth changed event this._triggerAuthChanged(); return result; } catch (error) { console.error('Login error:', error); throw error; } } /** * Logout a user * @returns {Promise<Object>} - Logout result */ async logout() { try { // Logout user with auth system if we have a refresh token if (this.refreshToken) { await this.auth.logout(this.refreshToken); } // Clear auth state this._clearAuthState(); return { success: true, message: 'Logout successful' }; } catch (error) { console.error('Logout error:', error); // Clear auth state even if logout fails this._clearAuthState(); throw error; } } /** * Refresh an access token * @returns {Promise<Object>} - Refresh result */ async refreshToken() { try { if (!this.refreshToken) { throw new Error('No refresh token available'); } // Refresh token with auth system const result = await this.auth.refreshToken(this.refreshToken); // Save new tokens this._saveTokensToLocalStorage(result.tokens); return result; } catch (error) { console.error('Token refresh error:', error); // Clear auth state if refresh fails this._clearAuthState(); throw error; } } /** * Validate an access token * @returns {Promise<Object|null>} - User data if token is valid, null otherwise */ async validateToken() { try { if (!this.accessToken) { return null; } // Validate token with auth system return await this.auth.validateToken(this.accessToken); } catch (error) { console.error('Token validation error:', error); return null; } } /** * Check authentication status * @returns {Promise<Object>} - Auth status */ async checkAuthStatus() { try { // Validate current token const user = await this.validateToken(); if (user) { return { authenticated: true, user, message: 'Authenticated' }; } else { // Try to refresh token if (this.refreshToken) { try { await this.refreshToken(); // Validate again after refresh const refreshedUser = await this.validateToken(); if (refreshedUser) { return { authenticated: true, user: refreshedUser, message: 'Authenticated after token refresh' }; } } catch (refreshError) { console.error('Error refreshing token:', refreshError); } } // If we get here, authentication failed this._clearAuthState(); return { authenticated: false, user: null, message: 'Not authenticated' }; } } catch (error) { console.error('Auth status check error:', error); // Clear auth state if check fails this._clearAuthState(); return { authenticated: false, user: null, message: 'Error checking authentication status' }; } } /** * Request a password reset * @param {string} email - User email * @returns {Promise<Object>} - Password reset result */ async resetPassword(email) { try { return await this.auth.resetPassword(email); } catch (error) { console.error('Password reset error:', error); throw error; } } /** * Confirm a password reset * @param {Object} resetData - Password reset data * @param {string} resetData.token - Password reset token * @param {string} resetData.password - New password * @returns {Promise<Object>} - Password reset result */ async resetPasswordConfirm(resetData) { try { return await this.auth.resetPasswordConfirm(resetData); } catch (error) { console.error('Password reset confirmation error:', error); throw error; } } /** * Change a user's password * @param {Object} passwordData - Password change data * @param {string} passwordData.currentPassword - Current password * @param {string} passwordData.newPassword - New password * @returns {Promise<Object>} - Password change result */ async changePassword(passwordData) { try { if (!this.user || !this.user.id) { throw new Error('User not authenticated'); } return await this.auth.changePassword({ userId: this.user.id, currentPassword: passwordData.currentPassword, newPassword: passwordData.newPassword }); } catch (error) { console.error('Password change error:', error); throw error; } } /** * Update a user's profile * @param {Object} profileData - Profile update data * @returns {Promise<Object>} - Profile update result */ async updateProfile(profileData) { try { if (!this.user || !this.user.id) { throw new Error('User not authenticated'); } const result = await this.auth.updateProfile({ userId: this.user.id, profile: profileData }); // Update user in localStorage this._saveUserToLocalStorage(result.user); return result; } catch (error) { console.error('Profile update error:', error); throw error; } } /** * Get a user's profile * @returns {Promise<Object>} - User profile */ async getProfile() { try { if (!this.user || !this.user.id) { throw new Error('User not authenticated'); } const result = await this.auth.getProfile(this.user.id); // Update user in localStorage this._saveUserToLocalStorage(result.user); return result; } catch (error) { console.error('Get profile error:', error); throw error; } } /** * Verify a user's email * @param {string} token - Email verification token * @returns {Promise<Object>} - Email verification result */ async verifyEmail(token) { try { const result = await this.auth.verifyEmail(token); // If verification successful, save tokens and user data if (result.tokens) { this._saveTokensToLocalStorage(result.tokens); this._saveUserToLocalStorage(result.user); // Trigger auth changed event this._triggerAuthChanged(); } return result; } catch (error) { console.error('Email verification error:', error); throw error; } } /** * Delete a user's account * @returns {Promise<boolean>} - Whether the account was deleted */ async deleteAccount() { try { if (!this.user || !this.user.id) { throw new Error('User not authenticated'); } // Delete user with auth system const result = await this.auth.adapter.deleteUser(this.user.id); // Clear auth state this._clearAuthState(); return result; } catch (error) { console.error('Account deletion error:', error); throw error; } } /** * Create a subscription for a user * @param {Object} subscriptionData - Subscription data * @returns {Promise<Object>} - Subscription result */ async createSubscription(subscriptionData) { try { if (!this.subscriptionApi || typeof this.subscriptionApi.createSubscription !== 'function') { throw new Error('Subscription API not available'); } if (!this.user || !this.user.email) { throw new Error('User not authenticated'); } // Create subscription with subscription API const result = await this.subscriptionApi.createSubscription( this.user.email, subscriptionData.plan, subscriptionData.paymentMethod ); // Update user with subscription data if (result && result.subscription) { const updatedUser = { ...this.user, subscription: { plan: result.subscription.plan || subscriptionData.plan, status: result.subscription.status || 'pending', expiresAt: result.subscription.expires_at || null } }; this._saveUserToLocalStorage(updatedUser); localStorage.setItem('subscription_data', JSON.stringify(result)); } return result; } catch (error) { console.error('Subscription creation error:', error); throw error; } } /** * Check subscription status * @returns {Promise<Object>} - Subscription status */ async checkSubscriptionStatus() { try { if (!this.subscriptionApi || typeof this.subscriptionApi.checkSubscriptionStatus !== 'function') { throw new Error('Subscription API not available'); } if (!this.user || !this.user.email) { throw new Error('User not authenticated'); } // Check subscription status with subscription API const result = await this.subscriptionApi.checkSubscriptionStatus(this.user.email); // Update user with subscription data if (result) { const updatedUser = { ...this.user, subscription: { plan: result.plan || 'unknown', status: result.status || 'unknown', expiresAt: result.expires_at || null } }; this._saveUserToLocalStorage(updatedUser); localStorage.setItem('subscription_data', JSON.stringify(result)); } return result; } catch (error) { console.error('Subscription status check error:', error); throw error; } } } export default AuthClient;