@gsarthak783/accesskit-auth
Version:
JavaScript/TypeScript SDK for AccessKit Authentication System - Easy auth integration for any project
652 lines (648 loc) • 22.7 kB
JavaScript
import axios from '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');
}
}
}
export { AuthClient, CookieTokenStorage, LocalTokenStorage, MemoryTokenStorage, AuthClient as default };
//# sourceMappingURL=index.esm.js.map