UNPKG

@gsarthak783/accesskit-auth

Version:

JavaScript/TypeScript SDK for AccessKit Authentication System - Easy auth integration for any project

660 lines (654 loc) 22.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var axios = require('axios'); /** * Default localStorage-based token storage * Works in browsers and React Native with AsyncStorage polyfill */ class LocalTokenStorage { constructor(keyPrefix = 'auth') { this.accessTokenKey = `${keyPrefix}_access_token`; this.refreshTokenKey = `${keyPrefix}_refresh_token`; } getAccessToken() { if (typeof localStorage === 'undefined') return null; return localStorage.getItem(this.accessTokenKey); } setAccessToken(token) { if (typeof localStorage === 'undefined') return; localStorage.setItem(this.accessTokenKey, token); } getRefreshToken() { if (typeof localStorage === 'undefined') return null; return localStorage.getItem(this.refreshTokenKey); } setRefreshToken(token) { if (typeof localStorage === 'undefined') return; localStorage.setItem(this.refreshTokenKey, token); } clearTokens() { if (typeof localStorage === 'undefined') return; localStorage.removeItem(this.accessTokenKey); localStorage.removeItem(this.refreshTokenKey); } } /** * Memory-based token storage (tokens lost on page refresh) * Useful for server-side rendering or when localStorage is not available */ class MemoryTokenStorage { constructor() { this.accessToken = null; this.refreshToken = null; } getAccessToken() { return this.accessToken; } setAccessToken(token) { this.accessToken = token; } getRefreshToken() { return this.refreshToken; } setRefreshToken(token) { this.refreshToken = token; } clearTokens() { this.accessToken = null; this.refreshToken = null; } } /** * Cookie-based token storage (for SSR or when you prefer cookies) * Note: Requires proper HTTPS and SameSite configuration in production */ class CookieTokenStorage { constructor(keyPrefix = 'auth') { this.accessTokenKey = `${keyPrefix}_access_token`; this.refreshTokenKey = `${keyPrefix}_refresh_token`; } getCookie(name) { if (typeof document === 'undefined') return null; const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { return parts.pop()?.split(';').shift() || null; } return null; } setCookie(name, value, days = 7) { if (typeof document === 'undefined') return; const expires = new Date(); expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`; } deleteCookie(name) { if (typeof document === 'undefined') return; document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`; } getAccessToken() { return this.getCookie(this.accessTokenKey); } setAccessToken(token) { this.setCookie(this.accessTokenKey, token, 1); // 1 day for access token } getRefreshToken() { return this.getCookie(this.refreshTokenKey); } setRefreshToken(token) { this.setCookie(this.refreshTokenKey, token, 7); // 7 days for refresh token } clearTokens() { this.deleteCookie(this.accessTokenKey); this.deleteCookie(this.refreshTokenKey); } } class AuthClient { constructor(config, storage) { this.eventListeners = new Map(); this.refreshPromise = null; this.currentUser = null; this.initialized = false; this.initPromise = null; this.config = { baseUrl: 'https://access-kit-server.vercel.app/api/project-users', projectId: '', timeout: 10000, ...config }; this.storage = storage || new LocalTokenStorage(); this.http = this.createHttpClient(); this.setupInterceptors(); // Auto-initialize to check existing auth state this.initialize(); } /** * Initialize auth state by checking stored tokens */ async initialize() { if (this.initialized || this.initPromise) { return this.initPromise || Promise.resolve(); } this.initPromise = (async () => { try { const accessToken = this.storage.getAccessToken(); const refreshToken = this.storage.getRefreshToken(); if (!accessToken && !refreshToken) { // No tokens, user is not authenticated this.currentUser = null; this.emit('authStateChange', { user: undefined, isAuthenticated: false, timestamp: Date.now() }); return; } // We have tokens, try to get user profile try { if (accessToken) { // First try with access token const user = await this.getProfile(); this.currentUser = user; this.emit('authStateChange', { user, isAuthenticated: true, timestamp: Date.now() }); } else if (refreshToken) { // No access token but have refresh token, try to refresh await this.refreshToken(); // After refresh, get user profile const user = await this.getProfile(); this.currentUser = user; this.emit('authStateChange', { user, isAuthenticated: true, timestamp: Date.now() }); } } catch (error) { // Both access and refresh failed, user is logged out this.storage.clearTokens(); this.currentUser = null; this.emit('authStateChange', { user: undefined, isAuthenticated: false, timestamp: Date.now() }); } } catch (error) { // Any other error, ensure clean state this.storage.clearTokens(); this.currentUser = null; this.emit('authStateChange', { user: undefined, isAuthenticated: false, timestamp: Date.now() }); } finally { this.initialized = true; this.initPromise = null; } })(); return this.initPromise; } /** * Get the current authenticated user (from memory, no API call) */ getCurrentUser() { return this.currentUser; } /** * Check if user is authenticated (has valid tokens) */ isAuthenticated() { return !!this.storage.getAccessToken() && !!this.currentUser; } /** * Subscribe to auth state changes * Returns an unsubscribe function */ onAuthStateChange(callback) { const listener = (data) => { if ('user' in data && 'isAuthenticated' in data) { callback(data.user, data.isAuthenticated); } }; this.on('authStateChange', listener); // Wait for initialization then call with current state if (this.initialized) { // Already initialized, call immediately callback(this.currentUser, this.isAuthenticated()); } else { // Wait for initialization to complete this.initialize().then(() => { callback(this.currentUser, this.isAuthenticated()); }).catch(() => { // Even if initialization fails, still call the callback callback(null, false); }); } // Return unsubscribe function return () => { this.off('authStateChange', listener); }; } /** * Create axios instance with default configuration */ createHttpClient() { return axios.create({ baseURL: this.config.baseUrl, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'X-API-Key': this.config.apiKey } }); } /** * Setup request/response interceptors for automatic token handling */ setupInterceptors() { // Request interceptor - add auth token this.http.interceptors.request.use((config) => { const token = this.storage.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Response interceptor - handle token refresh this.http.interceptors.response.use((response) => response, async (error) => { const originalRequest = error.config; // Skip refresh for auth endpoints const authEndpoints = ['/login', '/register', '/logout', '/refresh', '/request-password-reset', '/reset-password']; const isAuthEndpoint = authEndpoints.some(endpoint => originalRequest?.url?.includes(endpoint)); if (error.response?.status === 401 && !originalRequest._retry && !isAuthEndpoint) { originalRequest._retry = true; try { const newToken = await this.refreshToken(); originalRequest.headers.Authorization = `Bearer ${newToken}`; return this.http(originalRequest); } catch (refreshError) { this.logout(); this.emit('error', { error: refreshError, timestamp: Date.now() }); throw refreshError; } } throw error; }); } /** * Event system for auth state changes */ on(event, listener) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(listener); } off(event, listener) { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } } emit(event, data) { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach(listener => listener(data)); } } /** * Register a new user */ async register(userData) { try { const response = await this.http.post('/register', userData); if (response.data.success && response.data.data) { this.storage.setAccessToken(response.data.data.tokens.accessToken); this.storage.setRefreshToken(response.data.data.tokens.refreshToken); this.currentUser = response.data.data.user; this.emit('register', { user: response.data.data.user, timestamp: Date.now() }); this.emit('authStateChange', { user: response.data.data.user, isAuthenticated: true, timestamp: Date.now() }); } return response.data; } catch (error) { const authError = new Error(error.response?.data?.message || 'Registration failed'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Login user */ async login(credentials) { try { const response = await this.http.post('/login', credentials); if (response.data.success && response.data.data) { this.storage.setAccessToken(response.data.data.tokens.accessToken); this.storage.setRefreshToken(response.data.data.tokens.refreshToken); this.currentUser = response.data.data.user; this.emit('login', { user: response.data.data.user, timestamp: Date.now() }); this.emit('authStateChange', { user: response.data.data.user, isAuthenticated: true, timestamp: Date.now() }); } return response.data; } catch (error) { const authError = new Error(error.response?.data?.message || 'Login failed'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Logout user */ async logout() { try { const refreshToken = this.storage.getRefreshToken(); if (refreshToken) { await this.http.post('/logout', {}, { headers: { 'X-Refresh-Token': refreshToken } }); } } catch (error) { // Ignore logout errors, clear tokens anyway } finally { this.storage.clearTokens(); this.currentUser = null; this.emit('logout', { timestamp: Date.now() }); this.emit('authStateChange', { user: undefined, isAuthenticated: false, timestamp: Date.now() }); } } /** * Get current user profile */ async getProfile() { try { const response = await this.http.get('/profile'); if (response.data.success && response.data.data) { this.currentUser = response.data.data; return response.data.data; } throw new Error('Failed to get user profile'); } catch (error) { const authError = new Error(error.response?.data?.message || 'Failed to get profile'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Update user profile */ async updateProfile(data) { try { const response = await this.http.put('/profile', data); if (response.data.success && response.data.data) { this.currentUser = response.data.data; this.emit('profile_update', { user: response.data.data, timestamp: Date.now() }); this.emit('authStateChange', { user: response.data.data, isAuthenticated: true, timestamp: Date.now() }); return response.data.data; } throw new Error('Failed to update profile'); } catch (error) { const authError = new Error(error.response?.data?.message || 'Failed to update profile'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Refresh access token */ async refreshToken() { // Prevent multiple concurrent refresh requests if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise = this.performTokenRefresh(); try { const token = await this.refreshPromise; return token; } finally { this.refreshPromise = null; } } async performTokenRefresh() { const refreshToken = this.storage.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available'); } try { // The refresh endpoint returns data.accessToken and data.refreshToken const response = await this.http.post('/refresh', { refreshToken }); const newAccessToken = response.data.data.accessToken; const newRefreshToken = response.data.data.refreshToken; // Update both tokens this.storage.setAccessToken(newAccessToken); this.storage.setRefreshToken(newRefreshToken); this.emit('token_refresh', { timestamp: Date.now() }); return newAccessToken; } catch (error) { this.storage.clearTokens(); throw new Error('Token refresh failed'); } } /** * Request password reset */ async requestPasswordReset(email) { try { await this.http.post('/request-password-reset', { email }); } catch (error) { throw new Error(error.response?.data?.message || 'Password reset request failed'); } } /** * Reset password with token */ async resetPassword(token, password) { try { await this.http.post('/reset-password', { token, password }); } catch (error) { throw new Error(error.response?.data?.message || 'Password reset failed'); } } /** * Verify email address */ async verifyEmail(token) { try { await this.http.post('/verify-email', { token }); } catch (error) { throw new Error(error.response?.data?.message || 'Email verification failed'); } } /** * Update user password */ async updatePassword(data) { try { const response = await this.http.put('/change-password', data); // Password change invalidates all sessions, so clear tokens this.storage.clearTokens(); this.currentUser = null; this.emit('logout', { timestamp: Date.now() }); this.emit('authStateChange', { user: undefined, isAuthenticated: false, timestamp: Date.now() }); } catch (error) { const authError = new Error(error.response?.data?.message || 'Password update failed'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Update user email */ async updateEmail(data) { try { const response = await this.http.put('/update-email', data); if (response.data.success && response.data.data) { // Update current user's email if we have the user object if (this.currentUser) { this.currentUser.email = response.data.data.email; this.currentUser.isVerified = response.data.data.isVerified; this.emit('profile_update', { user: this.currentUser, timestamp: Date.now() }); this.emit('authStateChange', { user: this.currentUser, isAuthenticated: true, timestamp: Date.now() }); } return response.data.data; } throw new Error('Failed to update email'); } catch (error) { const authError = new Error(error.response?.data?.message || 'Email update failed'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Reauthenticate user with credentials * This is useful for sensitive operations that require password confirmation */ async reauthenticateWithCredential(data) { try { const response = await this.http.post('/reauthenticate', data); if (response.data.success && response.data.data) { this.emit('reauthenticate', { user: this.currentUser || undefined, timestamp: Date.now() }); return response.data.data; } throw new Error('Reauthentication failed'); } catch (error) { const authError = new Error(error.response?.data?.message || 'Reauthentication failed'); this.emit('error', { error: authError, timestamp: Date.now() }); throw authError; } } /** * Get current access token */ getAccessToken() { return this.storage.getAccessToken(); } /** * Export user data (requires admin access) */ async exportUsers(options = {}) { try { const response = await this.http.post('/export', options); return response.data; } catch (error) { throw new Error(error.response?.data?.message || 'Export failed'); } } /** * Import user data (requires admin access) */ async importUsers(data, options = {}) { try { const response = await this.http.post('/import', { data, options }); return response.data; } catch (error) { throw new Error(error.response?.data?.message || 'Import failed'); } } /** * Get all users (admin only, with pagination) */ async getUsers(options = {}) { try { const response = await this.http.get('/users', { params: options }); return response.data; } catch (error) { throw new Error(error.response?.data?.message || 'Failed to get users'); } } /** * Delete a user (admin only) */ async deleteUser(userId) { try { await this.http.delete(`/users/${userId}`); } catch (error) { throw new Error(error.response?.data?.message || 'Failed to delete user'); } } /** * Update user status (admin only) */ async updateUserStatus(userId, isActive) { try { const response = await this.http.patch(`/users/${userId}/status`, { isActive }); return response.data.data; } catch (error) { throw new Error(error.response?.data?.message || 'Failed to update user status'); } } /** * Get user by ID (admin only) */ async getUser(userId) { try { const response = await this.http.get(`/users/${userId}`); return response.data.data; } catch (error) { throw new Error(error.response?.data?.message || 'Failed to get user'); } } } exports.AuthClient = AuthClient; exports.CookieTokenStorage = CookieTokenStorage; exports.LocalTokenStorage = LocalTokenStorage; exports.MemoryTokenStorage = MemoryTokenStorage; exports.default = AuthClient; //# sourceMappingURL=index.js.map