@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
1,578 lines (1,459 loc) • 44.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "DeviceFingerprint", {
enumerable: true,
get: function () {
return _deviceManager.DeviceFingerprint;
}
});
Object.defineProperty(exports, "DeviceManager", {
enumerable: true,
get: function () {
return _deviceManager.DeviceManager;
}
});
exports.OxyServices = exports.OXY_CLOUD_URL = void 0;
Object.defineProperty(exports, "StoredDeviceInfo", {
enumerable: true,
get: function () {
return _deviceManager.StoredDeviceInfo;
}
});
exports.default = void 0;
var _axios = _interopRequireDefault(require("axios"));
var _jwtDecode = require("jwt-decode");
var _polyfills = require("../utils/polyfills");
var _deviceManager = require("../utils/deviceManager");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
let NodeFormData = null;
// Check if we're in Node.js environment
if (typeof window === 'undefined') {
try {
NodeFormData = require('form-data');
} catch (e) {
console.warn('form-data module not found, file uploads from Buffer may fail in Node.js');
}
}
// Import secure session types
/**
* Default cloud URL for Oxy services, cloud is where the user files are. (e.g. images, videos, etc.). Not the API.
*/
const OXY_CLOUD_URL = exports.OXY_CLOUD_URL = 'https://cloud.oxy.so';
// Export device management utilities
/**
* OxyServices - Client library for interacting with the Oxy API
*/
class OxyServices {
accessToken = null;
refreshToken = null;
refreshPromise = null;
/**
* Creates a new instance of the OxyServices client
* @param config - Configuration for the client
*/
constructor(config) {
this.client = _axios.default.create({
baseURL: config.baseURL,
timeout: 10000 // 10 second timeout
});
// Interceptor for adding auth header and handling token refresh
this.client.interceptors.request.use(async req => {
if (!this.accessToken) {
return req;
} // Check if token is expired and refresh if needed
try {
const decoded = (0, _jwtDecode.jwtDecode)(this.accessToken);
const currentTime = Math.floor(Date.now() / 1000);
// If token expires in less than 60 seconds, refresh it
if (decoded.exp - currentTime < 60) {
await this.refreshTokens();
}
} catch (error) {
// If token can't be decoded, continue with request and let server handle it
console.warn('Error decoding JWT token', error);
}
req.headers = req.headers || {};
req.headers.Authorization = `Bearer ${this.accessToken}`;
return req;
});
// Response interceptor for handling errors
this.client.interceptors.response.use(response => response, async error => {
const originalRequest = error.config;
// If the error is due to an expired token and we haven't tried refreshing yet
if (error.response?.status === 401 && this.refreshToken && originalRequest && !originalRequest.headers?.['X-Retry-After-Refresh']) {
try {
await this.refreshTokens();
// Retry the original request with new token
const newRequest = {
...originalRequest
};
if (newRequest.headers) {
newRequest.headers.Authorization = `Bearer ${this.accessToken}`;
newRequest.headers['X-Retry-After-Refresh'] = 'true';
}
return this.client(newRequest);
} catch (refreshError) {
// If refresh fails, force user to login again
this.clearTokens();
return Promise.reject(refreshError);
}
}
// Format error response
const apiError = {
message: error.response?.data?.error || error.response?.data?.message || 'An unknown error occurred',
code: error.response?.data?.code || 'UNKNOWN_ERROR',
status: error.response?.status || 500,
details: error.response?.data
};
// If the error is an invalid session, clear tokens
if (apiError.code === 'INVALID_SESSION' || apiError.message === 'Invalid session') {
this.clearTokens();
}
return Promise.reject(apiError);
});
}
/**
* Gets the base URL configured for this OxyServices instance
* @returns The base URL
*/
getBaseURL() {
return this.client.defaults.baseURL || '';
}
/**
* Gets the currently authenticated user ID from the token
* @returns The user ID or null if not authenticated
*/
getCurrentUserId() {
if (!this.accessToken) return null;
try {
const decoded = (0, _jwtDecode.jwtDecode)(this.accessToken);
// Check for both userId (preferred) and id (fallback) for compatibility
return decoded.userId || decoded.id || null;
} catch (error) {
return null;
}
}
/**
* Checks if the user is currently authenticated
* @returns Boolean indicating authentication status
*/
isAuthenticated() {
return this.accessToken !== null;
}
/**
* Sets authentication tokens directly (useful for initializing from storage)
* @param accessToken - JWT access token
* @param refreshToken - Refresh token for getting new access tokens
*/
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
/**
* Clears all authentication tokens
*/
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
}
/**
* Sign up a new user
* @param username - Desired username
* @param email - User's email address
* @param password - User's password
* @returns Object containing the message, token and user data
*/
async signUp(username, email, password) {
try {
const res = await this.client.post('/auth/signup', {
username,
email,
password
});
const {
message,
token,
user
} = res.data;
this.accessToken = token;
return {
message,
token,
user
};
} catch (error) {
throw this.handleError(error);
}
}
/**
* Log in and store tokens
* @param username - User's username or email
* @param password - User's password
* @returns Login response containing tokens and user data
*/
async login(username, password) {
try {
const res = await this.client.post('/auth/login', {
username,
password
});
const {
accessToken,
refreshToken,
user
} = res.data;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
return {
accessToken,
refreshToken,
user
};
} catch (error) {
throw this.handleError(error);
}
}
/**
* Log out user
*/
async logout() {
if (!this.refreshToken) return;
try {
await this.client.post('/auth/logout', {
refreshToken: this.refreshToken
});
} catch (error) {
console.warn('Error during logout', error);
} finally {
this.accessToken = null;
this.refreshToken = null;
}
}
/**
* Refresh access and refresh tokens
* @returns New tokens
*/
async refreshTokens() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
// If a refresh is already in progress, return that promise
if (this.refreshPromise) {
return this.refreshPromise;
}
// Create a new refresh promise
this.refreshPromise = (async () => {
try {
const res = await this.client.post('/auth/refresh', {
refreshToken: this.refreshToken
});
const {
accessToken,
refreshToken
} = res.data;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
return {
accessToken,
refreshToken
};
} catch (error) {
this.accessToken = null;
this.refreshToken = null;
throw this.handleError(error);
} finally {
this.refreshPromise = null;
}
})();
return this.refreshPromise;
}
/**
* Validate current access token
* @returns Boolean indicating if the token is valid
*/
async validate() {
try {
const res = await this.client.get('/auth/validate');
return res.data.valid;
} catch (error) {
return false;
}
}
/* Session Management Methods */
/**
* Get active sessions for the authenticated user
* @returns Array of active session objects
*/
async getUserSessions() {
try {
const res = await this.client.get('/sessions');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Logout from a specific session
* @param sessionId - The session ID to logout from
* @returns Success status
*/
async logoutSession(sessionId) {
try {
const res = await this.client.delete(`/sessions/${sessionId}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Logout from all other sessions (keep current session active)
* @returns Success status
*/
async logoutOtherSessions() {
try {
const res = await this.client.post('/sessions/logout-others');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Logout from all sessions
* @returns Success status
*/
async logoutAllSessions() {
try {
const res = await this.client.post('/sessions/logout-all');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get device sessions for a specific session ID
* @param sessionId - The session ID to get device sessions for
* @param deviceId - Optional device ID filter
* @returns Array of device sessions
*/
async getDeviceSessions(sessionId, deviceId) {
try {
const params = deviceId ? {
deviceId
} : {};
const res = await this.client.get(`/secure-session/device/sessions/${sessionId}`, {
params
});
// Map backend response to frontend interface
return (res.data.sessions || []).map(session => ({
sessionId: session.sessionId,
deviceId: res.data.deviceId || '',
deviceName: session.deviceInfo?.deviceName || 'Unknown Device',
isActive: true,
// All returned sessions are active
lastActive: session.lastActive,
expiresAt: session.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
isCurrent: session.sessionId === sessionId,
user: session.user,
createdAt: session.createdAt
}));
} catch (error) {
throw this.handleError(error);
}
}
/**
* Logout all device sessions for a specific device
* @param sessionId - The session ID
* @param deviceId - Optional device ID (uses current session's device if not provided)
* @param excludeCurrent - Whether to exclude the current session from logout
* @returns Logout response
*/
async logoutAllDeviceSessions(sessionId, deviceId, excludeCurrent) {
try {
const data = {};
if (deviceId) data.deviceId = deviceId;
if (excludeCurrent !== undefined) data.excludeCurrent = excludeCurrent;
const res = await this.client.post(`/secure-session/device/logout-all/${sessionId}`, data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Update device name for a session
* @param sessionId - The session ID
* @param deviceName - The new device name
* @returns Update response
*/
async updateDeviceName(sessionId, deviceName) {
try {
const res = await this.client.put(`/secure-session/device/name/${sessionId}`, {
deviceName
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* Profile Methods */
/**
* Fetch profile by username
* @param username - The username to look up
* @returns User profile data
*/
async getProfileByUsername(username) {
try {
const res = await this.client.get(`/profiles/username/${username}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Search profiles
* @param query - Search query string
* @param limit - Maximum number of results to return
* @param offset - Number of results to skip for pagination
* @returns Array of matching user profiles
*/
async searchProfiles(query, limit, offset) {
try {
const params = {
query
};
if (limit !== undefined) params.limit = limit;
if (offset !== undefined) params.offset = offset;
const res = await this.client.get('/profiles/search', {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get recommended profiles for the authenticated user
* @returns Array of recommended profiles
*/
async getProfileRecommendations() {
try {
const res = await this.client.get('/profiles/recommendations');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* User Methods */
/**
* Get general user by ID
* @param userId - The user ID to look up
* @returns User data
*/
async getUserById(userId) {
try {
const res = await this.client.get(`/users/${userId}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get the currently authenticated user's profile
* @returns User data for the current user
*/
async getCurrentUser() {
try {
const res = await this.client.get('/users/me');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Update the authenticated user's profile
* @param updates - Object containing fields to update
* @returns Updated user data
*/
async updateProfile(updates) {
try {
const res = await this.client.put('/users/me', updates);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Update user profile (requires auth)
* @param userId - User ID to update (must match authenticated user or have admin rights)
* @param updates - Object containing fields to update
* @returns Updated user data
*/
async updateUser(userId, updates) {
try {
const res = await this.client.put(`/users/${userId}`, updates);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Follow a user
* @param userId - User ID to follow
* @returns Status of the follow operation
*/
async followUser(userId) {
try {
const res = await this.client.post(`/users/${userId}/follow`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Unfollow a user
* @param userId - User ID to unfollow
* @returns Status of the unfollow operation
*/
async unfollowUser(userId) {
try {
const res = await this.client.delete(`/users/${userId}/follow`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get all followers of a user
* @param userId - User ID to get followers for
* @param limit - Maximum number of followers to return
* @param offset - Number of followers to skip for pagination
* @returns Array of users who follow the specified user and pagination info
*/
async getUserFollowers(userId, limit, offset) {
try {
const params = {};
if (limit !== undefined) params.limit = limit;
if (offset !== undefined) params.offset = offset;
const res = await this.client.get(`/users/${userId}/followers`, {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get all users that a user is following
* @param userId - User ID to get following list for
* @param limit - Maximum number of users to return
* @param offset - Number of users to skip for pagination
* @returns Array of users the specified user follows and pagination info
*/
async getUserFollowing(userId, limit, offset) {
try {
const params = {};
if (limit !== undefined) params.limit = limit;
if (offset !== undefined) params.offset = offset;
const res = await this.client.get(`/users/${userId}/following`, {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* Notification Methods */
/**
* Fetch all notifications for the authenticated user
* @returns Array of notifications
*/
async getNotifications() {
try {
const res = await this.client.get('/notifications');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get count of unread notifications
* @returns Number of unread notifications
*/
async getUnreadCount() {
try {
const res = await this.client.get('/notifications/unread-count');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Create a new notification (admin use)
* @param data - Notification data
* @returns Created notification
*/
async createNotification(data) {
try {
const res = await this.client.post('/notifications', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Mark a single notification as read
* @param notificationId - ID of notification to mark as read
*/
async markNotificationAsRead(notificationId) {
try {
await this.client.put(`/notifications/${notificationId}/read`);
} catch (error) {
throw this.handleError(error);
}
}
/**
* Mark all notifications as read
*/
async markAllNotificationsAsRead() {
try {
await this.client.put('/notifications/read-all');
} catch (error) {
throw this.handleError(error);
}
}
/**
* Delete a notification
* @param notificationId - ID of notification to delete
*/
async deleteNotification(notificationId) {
try {
await this.client.delete(`/notifications/${notificationId}`);
} catch (error) {
throw this.handleError(error);
}
}
/* Payment Methods */
/**
* Process a payment
* @param data - Payment data including user ID, plan, and payment method
* @returns Payment result with transaction ID
*/
async processPayment(data) {
try {
const res = await this.client.post('/payments/process', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Validate a payment method
* @param paymentMethod - Payment method to validate
* @returns Object indicating if the payment method is valid
*/
async validatePaymentMethod(paymentMethod) {
try {
const res = await this.client.post('/payments/validate', {
paymentMethod
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get saved payment methods for a user
* @param userId - User ID to get payment methods for
* @returns Array of payment methods
*/
async getPaymentMethods(userId) {
try {
const res = await this.client.get(`/payments/methods/${userId}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* Analytics Methods */
/**
* Get analytics data
* @param userId - User ID to get analytics for
* @param period - Time period for analytics (e.g., "day", "week", "month")
* @returns Analytics data
*/
async getAnalytics(userId, period) {
try {
const params = {
userID: userId
};
if (period) params.period = period;
const res = await this.client.get('/analytics', {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Update analytics (internal use)
* @param userId - User ID to update analytics for
* @param type - Type of analytics to update
* @param data - Analytics data to update
* @returns Message indicating success
*/
async updateAnalytics(userId, type, data) {
try {
const res = await this.client.post('/analytics/update', {
userID: userId,
type,
data
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get content viewers analytics
* @param userId - User ID to get viewer data for
* @param period - Time period for analytics
* @returns Array of content viewer data
*/
async getContentViewers(userId, period) {
try {
const params = {
userID: userId
};
if (period) params.period = period;
const res = await this.client.get('/analytics/viewers', {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get follower analytics details
* @param userId - User ID to get follower data for
* @param period - Time period for follower data
* @returns Follower details
*/
async getFollowerDetails(userId, period) {
try {
const params = {
userID: userId
};
if (period) params.period = period;
const res = await this.client.get('/analytics/followers', {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* Wallet Methods */
/**
* Get wallet info
* @param userId - User ID to get wallet for
* @returns Wallet data
*/
async getWallet(userId) {
try {
const res = await this.client.get(`/wallet/${userId}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get transaction history
* @param userId - User ID to get transactions for
* @param limit - Maximum number of transactions to return
* @param offset - Number of transactions to skip for pagination
* @returns Array of transactions and pagination info
*/
async getTransactionHistory(userId, limit, offset) {
try {
const params = {};
if (limit !== undefined) params.limit = limit;
if (offset !== undefined) params.offset = offset;
const res = await this.client.get(`/wallet/transactions/${userId}`, {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get a specific transaction
* @param transactionId - ID of transaction to retrieve
* @returns Transaction data
*/
async getTransaction(transactionId) {
try {
const res = await this.client.get(`/wallet/transaction/${transactionId}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Transfer funds between users
* @param data - Transfer details including source, destination, and amount
* @returns Transaction response
*/
async transferFunds(data) {
try {
const res = await this.client.post('/wallet/transfer', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Process a purchase
* @param data - Purchase details including user, item, and amount
* @returns Transaction response
*/
async processPurchase(data) {
try {
const res = await this.client.post('/wallet/purchase', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Request a withdrawal
* @param data - Withdrawal details including user, amount, and address
* @returns Transaction response
*/
async requestWithdrawal(data) {
try {
const res = await this.client.post('/wallet/withdraw', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* Karma Methods */
/**
* Get karma leaderboard
* @returns Array of karma leaderboard entries
*/
async getKarmaLeaderboard() {
try {
const res = await this.client.get('/karma/leaderboard');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get karma rules
* @returns Array of karma rules
*/
async getKarmaRules() {
try {
const res = await this.client.get('/karma/rules');
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get total karma for a user
* @param userId - User ID to get karma for
* @returns Object with total karma points
*/
async getUserKarmaTotal(userId) {
try {
const res = await this.client.get(`/karma/${userId}/total`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get karma history for a user
* @param userId - User ID to get karma history for
* @param limit - Maximum number of history entries to return
* @param offset - Number of entries to skip for pagination
* @returns Karma history entries and pagination info
*/
async getUserKarmaHistory(userId, limit, offset) {
try {
const params = {};
if (limit !== undefined) params.limit = limit;
if (offset !== undefined) params.offset = offset;
const res = await this.client.get(`/karma/${userId}/history`, {
params
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Award karma points to a user
* @param data - Karma award details
* @returns Karma award response
*/
async awardKarma(data) {
try {
const res = await this.client.post('/karma/award', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Deduct karma points from a user
* @param data - Karma deduction details
* @returns Karma deduction response
*/
async deductKarma(data) {
try {
const res = await this.client.post('/karma/deduct', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Create or update karma rule (admin)
* @param data - Karma rule data
* @returns Created or updated karma rule
*/
async createOrUpdateKarmaRule(data) {
try {
const res = await this.client.post('/karma/rules', data);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/* File Management Methods */
/**
* Upload a file using GridFS
* @param file - The file to upload (File or Blob in browser, Buffer in Node.js)
* @param filename - The name of the file
* @param metadata - Optional metadata to associate with the file
* @returns File metadata including ID and download URL
*/
async uploadFile(file,
// Use 'any' to handle Buffer type in cross-platform scenarios
filename, metadata) {
const response = await this.uploadFiles([file], [filename], metadata);
return response.files[0];
}
/**
* Upload multiple files using GridFS
* @param files - Array of files to upload
* @param filenames - Array of filenames (must match files array length)
* @param metadata - Optional metadata to associate with all files
* @returns Array of file metadata
*/
async uploadFiles(files, filenames, metadata) {
try {
if (files.length !== filenames.length) {
throw new Error('Files and filenames arrays must have the same length');
}
// Create form data to handle the file upload
let formData;
if (typeof window === 'undefined' && NodeFormData) {
// Node.js environment - prefer node-specific form-data
formData = new NodeFormData();
} else {
// Browser/React Native environment - use polyfilled or native FormData
const FormDataConstructor = (0, _polyfills.getFormDataConstructor)();
formData = new FormDataConstructor();
}
// Add all files to the form data
files.forEach((file, index) => {
const filename = filenames[index];
// Handle different file types (Browser vs Node.js vs React Native)
const isNodeBuffer = typeof window === 'undefined' && file && typeof file.constructor === 'function' && file.constructor.name === 'Buffer';
if (isNodeBuffer) {
// Node.js environment with Buffer
if (!NodeFormData) {
throw new Error('form-data module is required for file uploads from Buffer but not found.');
}
// form-data handles Buffers directly.
formData.append('files', file, {
filename
}); // Pass filename in options for form-data
} else {
// Browser/React Native environment with File or Blob
formData.append('files', file, filename);
}
});
// Add metadata as JSON string if provided
if (metadata) {
formData.append('metadata', JSON.stringify(metadata));
}
const res = await this.client.post('/files/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get file metadata by ID
* @param fileId - ID of the file to retrieve metadata for
* @returns File metadata
*/
async getFileMetadata(fileId) {
try {
const res = await this.client.get(`/files/${fileId}/metadata`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Update file metadata
* @param fileId - ID of the file to update
* @param updates - Metadata updates to apply
* @returns Updated file metadata
*/
async updateFileMetadata(fileId, updates) {
try {
const res = await this.client.put(`/files/${fileId}/metadata`, updates);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Delete a file by ID
* @param fileId - ID of the file to delete
* @returns Status of the delete operation
*/
async deleteFile(fileId) {
try {
console.log('Deleting file with ID:', fileId);
const res = await this.client.delete(`/files/${fileId}`);
console.log('Delete response:', res.data);
return res.data;
} catch (error) {
console.error('Delete file error:', error);
console.error('Error response:', error.response?.data);
console.error('Error status:', error.response?.status);
// Provide more specific error messages based on status code
if (error.response?.status === 404) {
throw new Error('File not found or already deleted');
} else if (error.response?.status === 403) {
throw new Error('You do not have permission to delete this file');
} else if (error.response?.status === 400) {
throw new Error('Invalid file ID format');
}
throw this.handleError(error);
}
}
/**
* Get download URL for a file
* @param fileId - ID of the file to get download URL for
* @returns Full URL to download the file
*/
getFileDownloadUrl(fileId) {
return `${this.client.defaults.baseURL}/files/${fileId}`;
}
/**
* Stream a file (useful for playing audio/video without full download)
* @param fileId - ID of the file to stream
* @returns Full URL to stream the file
*/
getFileStreamUrl(fileId) {
return `${this.client.defaults.baseURL}/files/${fileId}`;
}
/**
* List files for a specific user
* @param userId - User ID to list files for
* @param limit - Maximum number of files to return
* @param offset - Number of files to skip for pagination
* @param filters - Optional filters for the file list (e.g., contentType)
* @returns Array of file metadata and pagination info
*/
async listUserFiles(userId, limit, offset, filters) {
try {
const params = {};
if (limit !== undefined) params.limit = limit;
if (offset !== undefined) params.offset = offset;
if (filters) Object.assign(params, filters);
const res = await this.client.get(`/files/list/${userId}`, {
params
});
// Handle backend response format: backend returns FileMetadata[] directly
// but interface expects { files: FileMetadata[], total: number, hasMore: boolean }
const rawFiles = Array.isArray(res.data) ? res.data : res.data.files || [];
// Transform GridFS files to match FileMetadata interface (map _id to id)
const filesArray = rawFiles.map(file => ({
...file,
id: file._id?.toString() || file.id,
uploadDate: file.uploadDate?.toISOString ? file.uploadDate.toISOString() : file.uploadDate
}));
return {
files: filesArray,
total: filesArray.length,
hasMore: false // No pagination in current backend implementation
};
} catch (error) {
throw this.handleError(error);
}
}
/**
* Secure login that returns only session data (no tokens stored locally)
* @param username - User's username or email
* @param password - User's password
* @param deviceName - Optional device name for session tracking
* @param deviceFingerprint - Device fingerprint for enhanced security
* @returns Secure login response with session data
*/
async secureLogin(username, password, deviceName, deviceFingerprint) {
try {
const payload = {
username,
password,
deviceName
};
if (deviceFingerprint) {
payload.deviceFingerprint = deviceFingerprint;
}
const res = await this.client.post('/secure-session/login', payload);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get full user data by session ID
* @param sessionId - The session ID
* @returns Full user data
*/
async getUserBySession(sessionId) {
try {
const res = await this.client.get(`/secure-session/user/${sessionId}`);
return res.data.user;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get access token by session ID (for API calls)
* @param sessionId - The session ID
* @returns Access token and expiry info
*/
async getTokenBySession(sessionId) {
try {
const res = await this.client.get(`/secure-session/token/${sessionId}`);
// Set the token for subsequent API calls
this.accessToken = res.data.accessToken;
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Get all active sessions for current user
* @param sessionId - Current session ID
* @returns Array of user sessions
*/
async getSessionsBySessionId(sessionId) {
try {
const res = await this.client.get(`/secure-session/sessions/${sessionId}`);
return res.data.sessions;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Logout specific session
* @param sessionId - Current session ID
* @param targetSessionId - Optional target session to logout (defaults to current)
*/
async logoutSecureSession(sessionId, targetSessionId) {
try {
await this.client.post(`/secure-session/logout/${sessionId}`, {
targetSessionId
});
// If we're logging out the current session, clear the access token
if (!targetSessionId || targetSessionId === sessionId) {
this.accessToken = null;
this.refreshToken = null;
}
} catch (error) {
throw this.handleError(error);
}
}
/**
* Logout all sessions for current user
* @param sessionId - Current session ID
*/
async logoutAllSecureSessions(sessionId) {
console.log('logoutAllSecureSessions called with sessionId:', sessionId);
console.log('API client defaults:', this.client.defaults);
try {
const response = await this.client.post(`/secure-session/logout-all/${sessionId}`);
console.log('logoutAllSecureSessions response:', response.status, response.data);
// Clear tokens since all sessions are logged out
this.accessToken = null;
this.refreshToken = null;
console.log('Tokens cleared successfully');
} catch (error) {
console.error('logoutAllSecureSessions error:', error);
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error;
console.error('Error response data:', axiosError.response?.data);
console.error('Error response status:', axiosError.response?.status);
}
throw this.handleError(error);
}
}
/**
* Validate session
* @param sessionId - The session ID to validate
* @returns Session validation status with user data
*/
async validateSession(sessionId) {
try {
const res = await this.client.get(`/secure-session/validate/${sessionId}`);
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Validate session using x-session-id header
* @param sessionId - The session ID to validate (sent as header)
* @param deviceFingerprint - Optional device fingerprint for enhanced security
* @returns Session validation status with user data
*/
async validateSessionFromHeader(sessionId, deviceFingerprint) {
try {
const headers = {
'x-session-id': sessionId
};
if (deviceFingerprint) {
headers['x-device-fingerprint'] = deviceFingerprint;
}
const res = await this.client.get('/secure-session/validate-header', {
headers
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Validate session using automatic header detection
* The validateSession endpoint will automatically read from x-session-id header
* @param sessionId - The session ID to validate (sent as header)
* @param deviceFingerprint - Optional device fingerprint for enhanced security
* @returns Session validation status with user data
*/
async validateSessionAuto(sessionId, deviceFingerprint) {
try {
const headers = {
'x-session-id': sessionId
};
if (deviceFingerprint) {
headers['x-device-fingerprint'] = deviceFingerprint;
}
// Call the regular validateSession endpoint which now auto-reads from headers
// Use 'auto' as placeholder since the controller reads from header
const res = await this.client.get('/secure-session/validate/auto', {
headers
});
return res.data;
} catch (error) {
throw this.handleError(error);
}
}
/**
* Utility method to help implement authentication middleware in Express.js applications
* This creates a function that can be used as Express middleware to validate tokens
* @param options - Configuration options for the middleware
* @returns Express middleware function
*/
createAuthenticateTokenMiddleware(options = {}) {
const {
loadFullUser = true,
onError
} = options;
return async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
const error = {
message: 'Access token required',
code: 'MISSING_TOKEN',
status: 401
};
if (onError) {
return onError(error);
}
return res.status(401).json({
message: 'Access token required',
code: 'MISSING_TOKEN'
});
}
// Create a temporary OxyServices instance with the token to validate it
const tempOxyServices = new OxyServices({
baseURL: this.client.defaults.baseURL || ''
});
tempOxyServices.setTokens(token, ''); // Set access token
// Validate token using the validate method
const isValid = await tempOxyServices.validate();
if (!isValid) {
const error = {
message: 'Invalid or expired token',
code: 'INVALID_TOKEN',
status: 403
};
if (onError) {
return onError(error);
}
return res.status(403).json({
message: 'Invalid or expired token',
code: 'INVALID_TOKEN'
});
}
// Get user ID from token
const userId = tempOxyServices.getCurrentUserId();
if (!userId) {
const error = {
message: 'Invalid token payload',
code: 'INVALID_PAYLOAD',
status: 403
};
if (onError) {
return onError(error);
}
return res.status(403).json({
message: 'Invalid token payload',
code: 'INVALID_PAYLOAD'
});
}
// Set user information on request object
req.userId = userId;
req.accessToken = token;
// Optionally load full user data
if (loadFullUser) {
try {
const userProfile = await tempOxyServices.getUserById(userId);
req.user = userProfile;
} catch (userError) {
// If we can't load user, continue with just ID
req.user = {
id: userId
};
}
} else {
req.user = {
id: userId
};
}
next();
} catch (error) {
const apiError = this.handleError(error);
if (onError) {
return onError(apiError);
}
return res.status(apiError.status || 500).json({
message: apiError.message,
code: apiError.code
});
}
};
}
/**
* Helper method for validating tokens without Express middleware
* Useful for standalone token validation in various contexts
* @param token - The access token to validate
* @returns Object with validation result and user information
*/
async authenticateToken(token) {
try {
if (!token) {
return {
valid: false,
error: 'Token is required'
};
}
// Create a temporary OxyServices instance with the token
const tempOxyServices = new OxyServices({
baseURL: this.client.defaults.baseURL || ''
});
tempOxyServices.setTokens(token, '');
// Validate token
const isValid = await tempOxyServices.validate();
if (!isValid) {
return {
valid: false,
error: 'Invalid or expired token'
};
}
// Get user ID from token
const userId = tempOxyServices.getCurrentUserId();
if (!userId) {
return {
valid: false,
error: 'Invalid token payload'
};
}
// Try to get user profile
let user;
try {
user = await tempOxyServices.getUserById(userId);
} catch (error) {
// Continue without full user data
user = {
id: userId
};
}
return {
valid: true,
userId,
user
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : 'Token validation failed'
};
}
}
/**
* Centralized error handling
* @private
* @param error - Error object from API call
* @returns Formatted API error
*/
handleError(error) {
if (error && error.code && error.status) {
// Already formatted as ApiError
return error;
}
const apiError = {
message: error?.message || error?.response?.data?.message || 'Unknown error occurred',
code: error?.response?.data?.code || 'UNKNOWN_ERROR',
status: error?.response?.status || 500,
details: error?.response?.data
};
return apiError;
}
}
exports.OxyServices = OxyServices;
var _default = exports.default = OxyServices;
//# sourceMappingURL=index.js.map