Files
easystream-main/f_scripts/fe/js/api-helper.js
SamiAhmed7777 f0f346deb9
Some checks failed
EasyStream Test Suite / test (pull_request) Has been cancelled
EasyStream Test Suite / code-quality (pull_request) Has been cancelled
EasyStream Test Suite / integration-test (pull_request) Has been cancelled
Sync current dev state
2025-12-15 17:28:21 -08:00

699 lines
19 KiB
JavaScript

/**
* EasyStream API Helper
* Modern fetch-based API client with JWT token management
*/
class EasyStreamAPI {
constructor(baseURL = '/api') {
this.baseURL = baseURL;
this.token = this.getStoredToken();
this.tokenExpiry = this.getStoredTokenExpiry();
}
/**
* Store JWT token in localStorage
* @param {string} token JWT token
* @param {number} expiresIn Expiry time in seconds
*/
setToken(token, expiresIn = 86400) {
this.token = token;
this.tokenExpiry = Date.now() + (expiresIn * 1000);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('jwt_token', token);
localStorage.setItem('jwt_expiry', this.tokenExpiry.toString());
}
}
/**
* Get stored token from localStorage
* @returns {string|null}
*/
getStoredToken() {
if (typeof localStorage === 'undefined') return null;
const expiry = localStorage.getItem('jwt_expiry');
if (expiry && Date.now() > parseInt(expiry)) {
// Token expired, clear it
this.clearToken();
return null;
}
return localStorage.getItem('jwt_token');
}
/**
* Get stored token expiry
* @returns {number|null}
*/
getStoredTokenExpiry() {
if (typeof localStorage === 'undefined') return null;
const expiry = localStorage.getItem('jwt_expiry');
return expiry ? parseInt(expiry) : null;
}
/**
* Clear stored token
*/
clearToken() {
this.token = null;
this.tokenExpiry = null;
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('jwt_token');
localStorage.removeItem('jwt_expiry');
}
}
/**
* Check if user is authenticated
* @returns {boolean}
*/
isAuthenticated() {
if (!this.token) return false;
if (this.tokenExpiry && Date.now() > this.tokenExpiry) {
this.clearToken();
return false;
}
return true;
}
/**
* Get authorization headers
* @returns {Object}
*/
getAuthHeaders() {
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
return headers;
}
/**
* Make API request
* @param {string} endpoint API endpoint
* @param {Object} options Fetch options
* @returns {Promise<Object>}
*/
async request(endpoint, options = {}) {
const url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.getAuthHeaders(),
...options.headers
}
};
try {
const response = await fetch(url, config);
// Handle different response types
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
if (!response.ok) {
// Handle authentication errors
if (response.status === 401) {
this.clearToken();
throw new Error(data.message || 'Authentication failed');
}
throw new Error(data.message || `Request failed with status ${response.status}`);
}
return data;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
/**
* GET request
* @param {string} endpoint API endpoint
* @param {Object} params Query parameters
* @returns {Promise<Object>}
*/
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, {
method: 'GET'
});
}
/**
* POST request
* @param {string} endpoint API endpoint
* @param {Object} data Request body
* @returns {Promise<Object>}
*/
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* PUT request
* @param {string} endpoint API endpoint
* @param {Object} data Request body
* @returns {Promise<Object>}
*/
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
/**
* DELETE request
* @param {string} endpoint API endpoint
* @returns {Promise<Object>}
*/
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
// ===== Authentication Methods =====
/**
* Login with JWT token
* @param {string} identifier Username or email
* @param {string} password Password
* @param {number} expiresIn Optional token expiry in seconds
* @returns {Promise<Object>}
*/
async login(identifier, password, expiresIn = null) {
try {
const data = await this.post('/auth.php?action=login_token', {
identifier,
password,
expires_in: expiresIn
});
if (data.success && data.token) {
this.setToken(data.token, data.expires_in || 86400);
}
return data;
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
/**
* Logout and clear token
* @returns {Promise<Object>}
*/
async logout() {
try {
// Optionally call backend logout endpoint
// (though with JWT, client-side token removal is usually sufficient)
const data = await this.post('/auth.php?action=logout', {});
this.clearToken();
return data;
} catch (error) {
// Clear token even if API call fails
this.clearToken();
throw error;
}
}
/**
* Get current user info
* @returns {Promise<Object>}
*/
async getCurrentUser() {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated');
}
return this.get('/auth.php?action=verify_token');
}
/**
* Verify token validity
* @returns {Promise<boolean>}
*/
async verifyToken() {
if (!this.token) return false;
try {
const data = await this.get('/auth.php?action=verify_token');
return data.success && data.valid;
} catch (error) {
this.clearToken();
return false;
}
}
// ===== Video API Methods =====
/**
* Get videos list
* @param {Object} params Query parameters (page, limit, sort, etc.)
* @returns {Promise<Object>}
*/
async getVideos(params = {}) {
return this.get('/videos.php', params);
}
/**
* Get video by ID
* @param {string} videoId Video ID
* @returns {Promise<Object>}
*/
async getVideo(videoId) {
return this.get(`/videos.php?id=${videoId}`);
}
/**
* Upload video metadata
* @param {Object} videoData Video metadata
* @returns {Promise<Object>}
*/
async createVideo(videoData) {
return this.post('/videos.php', videoData);
}
/**
* Update video
* @param {string} videoId Video ID
* @param {Object} videoData Updated video data
* @returns {Promise<Object>}
*/
async updateVideo(videoId, videoData) {
return this.put(`/videos.php?id=${videoId}`, videoData);
}
/**
* Delete video
* @param {string} videoId Video ID
* @returns {Promise<Object>}
*/
async deleteVideo(videoId) {
return this.delete(`/videos.php?id=${videoId}`);
}
/**
* Search videos
* @param {string} query Search query
* @param {Object} params Additional parameters
* @returns {Promise<Object>}
*/
async searchVideos(query, params = {}) {
return this.get('/videos.php', { action: 'search', q: query, ...params });
}
/**
* Like/unlike video
* @param {string} fileKey Video file key
* @param {string} likeType 'like' or 'dislike'
* @returns {Promise<Object>}
*/
async likeVideo(fileKey, likeType = 'like') {
return this.post('/videos.php?action=like', { file_key: fileKey, like_type: likeType });
}
/**
* Increment video view count
* @param {string} fileKey Video file key
* @returns {Promise<Object>}
*/
async recordVideoView(fileKey) {
return this.post('/videos.php?action=view', { file_key: fileKey });
}
/**
* Add/remove video from watch later
* @param {string} fileKey Video file key
* @returns {Promise<Object>}
*/
async toggleWatchLater(fileKey) {
return this.post('/videos.php?action=watch_later', { file_key: fileKey });
}
// ===== User API Methods =====
/**
* Get user profile
* @param {string} userId User ID (optional, defaults to current user)
* @returns {Promise<Object>}
*/
async getUserProfile(userId = null) {
if (userId) {
return this.get(`/user.php?id=${userId}`);
}
return this.get('/user.php?action=profile');
}
/**
* Get current user's profile (alias)
* @returns {Promise<Object>}
*/
async getMyProfile() {
return this.get('/user.php?action=me');
}
/**
* Update user profile
* @param {Object} userData Updated user data
* @returns {Promise<Object>}
*/
async updateProfile(userData) {
return this.put('/user.php', userData);
}
/**
* Upload user avatar
* @param {File} file Avatar file
* @returns {Promise<Object>}
*/
async uploadAvatar(file) {
const formData = new FormData();
formData.append('avatar', file);
return this.request('/user.php?action=avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`
},
body: formData
});
}
/**
* Get user statistics
* @returns {Promise<Object>}
*/
async getUserStats() {
return this.get('/user.php?action=stats');
}
/**
* Get user's videos
* @param {number} userId User ID (optional)
* @param {Object} params Query parameters
* @returns {Promise<Object>}
*/
async getUserVideos(userId = null, params = {}) {
const queryParams = userId ? { ...params, id: userId } : params;
return this.get('/user.php?action=videos', queryParams);
}
/**
* Get user's subscriptions
* @returns {Promise<Object>}
*/
async getUserSubscriptions() {
return this.get('/user.php?action=subscriptions');
}
/**
* Get user's subscribers
* @param {number} userId User ID (optional)
* @returns {Promise<Object>}
*/
async getUserSubscribers(userId = null) {
const params = userId ? { id: userId } : {};
return this.get('/user.php?action=subscribers', params);
}
// ===== Comments API Methods =====
/**
* Get comments for a video
* @param {string} fileKey Video file key
* @param {Object} params Query parameters
* @returns {Promise<Object>}
*/
async getComments(fileKey, params = {}) {
return this.get('/comments.php', { file_key: fileKey, ...params });
}
/**
* Get single comment with replies
* @param {number} commentId Comment ID
* @returns {Promise<Object>}
*/
async getComment(commentId) {
return this.get(`/comments.php?id=${commentId}`);
}
/**
* Create new comment
* @param {string} fileKey Video file key
* @param {string} commentText Comment text
* @param {number} parentId Parent comment ID (for replies)
* @returns {Promise<Object>}
*/
async createComment(fileKey, commentText, parentId = null) {
return this.post('/comments.php?action=create', {
file_key: fileKey,
comment_text: commentText,
parent_id: parentId
});
}
/**
* Update comment
* @param {number} commentId Comment ID
* @param {string} commentText Updated text
* @returns {Promise<Object>}
*/
async updateComment(commentId, commentText) {
return this.put(`/comments.php?id=${commentId}`, {
comment_text: commentText
});
}
/**
* Delete comment
* @param {number} commentId Comment ID
* @returns {Promise<Object>}
*/
async deleteComment(commentId) {
return this.delete(`/comments.php?id=${commentId}`);
}
/**
* Like/unlike comment
* @param {number} commentId Comment ID
* @returns {Promise<Object>}
*/
async likeComment(commentId) {
return this.post('/comments.php?action=like', { comment_id: commentId });
}
/**
* Report comment
* @param {number} commentId Comment ID
* @param {string} reason Report reason
* @returns {Promise<Object>}
*/
async reportComment(commentId, reason) {
return this.post('/comments.php?action=report', {
comment_id: commentId,
reason: reason
});
}
// ===== Subscriptions API Methods =====
/**
* Get user's subscriptions
* @returns {Promise<Object>}
*/
async getSubscriptions() {
return this.get('/subscriptions.php?action=list');
}
/**
* Get channel's subscribers
* @param {number} channelId Channel ID
* @param {Object} params Query parameters
* @returns {Promise<Object>}
*/
async getSubscribers(channelId, params = {}) {
return this.get('/subscriptions.php?action=subscribers', {
channel_id: channelId,
...params
});
}
/**
* Get subscription feed (videos from subscribed channels)
* @param {Object} params Query parameters
* @returns {Promise<Object>}
*/
async getSubscriptionFeed(params = {}) {
return this.get('/subscriptions.php?action=feed', params);
}
/**
* Check if subscribed to a channel
* @param {number} channelId Channel ID
* @returns {Promise<Object>}
*/
async checkSubscription(channelId) {
return this.get('/subscriptions.php?action=check', { channel_id: channelId });
}
/**
* Subscribe to a channel
* @param {number} channelId Channel ID
* @returns {Promise<Object>}
*/
async subscribe(channelId) {
return this.post('/subscriptions.php', { channel_id: channelId });
}
/**
* Unsubscribe from a channel
* @param {number} channelId Channel ID
* @returns {Promise<Object>}
*/
async unsubscribe(channelId) {
return this.delete(`/subscriptions.php?channel_id=${channelId}`);
}
// ===== Social API Methods =====
/**
* Get social feed
* @param {Object} params Query parameters
* @returns {Promise<Object>}
*/
async getSocialFeed(params = {}) {
return this.get('/social.php', params);
}
// ===== Helper Methods =====
/**
* Upload file with progress tracking
* @param {string} endpoint Upload endpoint
* @param {File} file File to upload
* @param {Object} additionalData Additional form data
* @param {Function} onProgress Progress callback
* @returns {Promise<Object>}
*/
async uploadFile(endpoint, file, additionalData = {}, onProgress = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
// Add additional data
Object.keys(additionalData).forEach(key => {
formData.append(key, additionalData[key]);
});
// Progress tracking
if (onProgress && xhr.upload) {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete, e.loaded, e.total);
}
});
}
// Handle completion
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (e) {
resolve({ success: true, data: xhr.responseText });
}
} else {
try {
const error = JSON.parse(xhr.responseText);
reject(new Error(error.message || `Upload failed with status ${xhr.status}`));
} catch (e) {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
}
});
// Handle errors
xhr.addEventListener('error', () => {
reject(new Error('Upload failed due to network error'));
});
xhr.addEventListener('abort', () => {
reject(new Error('Upload aborted'));
});
// Add auth header
if (this.token) {
xhr.setRequestHeader('Authorization', `Bearer ${this.token}`);
}
// Send request
const url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`;
xhr.open('POST', url);
xhr.send(formData);
});
}
/**
* Handle API errors consistently
* @param {Error} error Error object
* @param {Function} callback Optional callback
*/
handleError(error, callback = null) {
console.error('API Error:', error);
// Show user-friendly error message
const message = error.message || 'An unexpected error occurred';
if (callback) {
callback(message);
} else if (typeof window !== 'undefined' && window.showNotification) {
window.showNotification('error', message);
} else {
alert(message);
}
return { success: false, error: message };
}
}
// Create global instance
if (typeof window !== 'undefined') {
window.EasyStreamAPI = EasyStreamAPI;
window.api = new EasyStreamAPI();
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = EasyStreamAPI;
}