Sync current dev state
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

This commit is contained in:
SamiAhmed7777
2025-12-15 17:28:21 -08:00
parent 3bf64b1058
commit f0f346deb9
54 changed files with 11060 additions and 484 deletions

581
api/user.php Normal file
View File

@@ -0,0 +1,581 @@
<?php
/**
* User API Endpoint
* Handles user profile and account operations
*
* Supported Actions:
* - GET profile: Get user profile
* - PUT update: Update user profile
* - POST avatar: Upload avatar
* - GET stats: Get user statistics
* - GET videos: Get user's videos
*/
// Include CORS configuration
require_once __DIR__ . '/cors.config.php';
header('Content-Type: application/json');
// Include core configuration
require_once dirname(__FILE__) . '/../f_core/config.core.php';
// Initialize response
$response = ['success' => false, 'data' => null, 'error' => null];
try {
// Get action from query parameter
$action = isset($_GET['action']) ? $_GET['action'] : 'profile';
$method = $_SERVER['REQUEST_METHOD'];
// Get authenticated user (supports both session and JWT)
$userId = null;
// Try JWT authentication first
$authHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : null);
if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
$token = $matches[1];
$tokenData = VAuth::verifyToken($token);
if ($tokenData && isset($tokenData['user_id'])) {
$userId = $tokenData['user_id'];
}
}
// Fall back to session authentication
if (!$userId && isset($_SESSION['USER_ID'])) {
$userId = $_SESSION['USER_ID'];
} elseif (!$userId && isset($_SESSION['usr_id'])) {
$userId = $_SESSION['usr_id'];
}
// Route based on method and action
switch ($method) {
case 'GET':
switch ($action) {
case 'profile':
case 'me':
handleGetProfile($userId);
break;
case 'stats':
handleGetStats($userId);
break;
case 'videos':
handleGetUserVideos($userId);
break;
case 'subscriptions':
handleGetSubscriptions($userId);
break;
case 'subscribers':
handleGetSubscribers($userId);
break;
default:
// Get another user's public profile
$targetUserId = isset($_GET['id']) ? (int)$_GET['id'] : null;
if ($targetUserId) {
handleGetPublicProfile($targetUserId, $userId);
} else {
handleGetProfile($userId);
}
}
break;
case 'POST':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
switch ($action) {
case 'avatar':
handleUploadAvatar($userId);
break;
case 'subscribe':
handleSubscribe($userId);
break;
case 'unsubscribe':
handleUnsubscribe($userId);
break;
default:
throw new Exception('Invalid action', 400);
}
break;
case 'PUT':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleUpdateProfile($userId);
break;
case 'DELETE':
if (!$userId) {
throw new Exception('Authentication required', 401);
}
handleDeleteAccount($userId);
break;
default:
throw new Exception('Method not allowed', 405);
}
} catch (Exception $e) {
http_response_code($e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500);
$response['error'] = $e->getMessage();
echo json_encode($response);
exit;
}
/**
* Get current user's profile
*/
function handleGetProfile($userId) {
global $class_database, $response;
if (!$userId) {
throw new Exception('Authentication required', 401);
}
$sql = "SELECT usr_id, usr_user, usr_dname, usr_email, usr_fname, usr_lname,
usr_avatar, usr_about, usr_website, usr_location,
usr_verified, usr_partner, usr_affiliate,
usr_joined, usr_lastlogin, usr_profile_privacy
FROM db_users
WHERE usr_id = ?";
$result = $class_database->execute($sql, [$userId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('User not found', 404);
}
$user = $result->fields;
// Get user stats
$statsSql = "SELECT
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1) as video_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?) as subscriber_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE usr_id = ?) as subscription_count,
(SELECT SUM(file_views) FROM db_videofiles WHERE usr_id = ?) as total_views
FROM dual";
$stats = $class_database->execute($statsSql, [$userId, $userId, $userId, $userId]);
if ($stats && $stats->RecordCount() > 0) {
$user['stats'] = [
'videos' => (int)$stats->fields['video_count'],
'subscribers' => (int)$stats->fields['subscriber_count'],
'subscriptions' => (int)$stats->fields['subscription_count'],
'views' => (int)$stats->fields['total_views']
];
}
// Get preferences if they exist
$prefSql = "SELECT * FROM db_user_preferences WHERE usr_id = ?";
$prefs = $class_database->execute($prefSql, [$userId]);
if ($prefs && $prefs->RecordCount() > 0) {
$user['preferences'] = $prefs->fields;
}
$response['success'] = true;
$response['data'] = $user;
echo json_encode($response);
}
/**
* Get another user's public profile
*/
function handleGetPublicProfile($targetUserId, $currentUserId) {
global $class_database, $response;
$sql = "SELECT usr_id, usr_user, usr_dname, usr_avatar, usr_about,
usr_website, usr_location, usr_verified, usr_partner,
usr_joined, usr_profile_privacy
FROM db_users
WHERE usr_id = ?";
$result = $class_database->execute($sql, [$targetUserId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('User not found', 404);
}
$user = $result->fields;
// Check privacy settings
if ($user['usr_profile_privacy'] === 'private' && $targetUserId != $currentUserId) {
throw new Exception('This profile is private', 403);
}
// Get public stats
$statsSql = "SELECT
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1 AND privacy = 'public') as video_count,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?) as subscriber_count,
(SELECT SUM(file_views) FROM db_videofiles WHERE usr_id = ? AND approved = 1) as total_views
FROM dual";
$stats = $class_database->execute($statsSql, [$targetUserId, $targetUserId, $targetUserId]);
if ($stats && $stats->RecordCount() > 0) {
$user['stats'] = [
'videos' => (int)$stats->fields['video_count'],
'subscribers' => (int)$stats->fields['subscriber_count'],
'views' => (int)$stats->fields['total_views']
];
}
// Check if current user is subscribed
if ($currentUserId) {
$subSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$subResult = $class_database->execute($subSql, [$currentUserId, $targetUserId]);
$user['is_subscribed'] = $subResult && $subResult->RecordCount() > 0;
}
$response['success'] = true;
$response['data'] = $user;
echo json_encode($response);
}
/**
* Update user profile
*/
function handleUpdateProfile($userId) {
global $class_database, $response;
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
throw new Exception('Invalid JSON input', 400);
}
// Build update query dynamically
$updates = [];
$params = [];
$allowedFields = [
'usr_dname', 'usr_fname', 'usr_lname', 'usr_about',
'usr_website', 'usr_location', 'usr_profile_privacy'
];
foreach ($allowedFields as $field) {
if (isset($input[$field])) {
$updates[] = "$field = ?";
$params[] = VSecurity::sanitize($input[$field]);
}
}
if (empty($updates)) {
throw new Exception('No fields to update', 400);
}
$params[] = $userId;
$sql = "UPDATE db_users SET " . implode(', ', $updates) . " WHERE usr_id = ?";
$result = $class_database->execute($sql, $params);
if (!$result) {
throw new Exception('Failed to update profile', 500);
}
// Log the update
VLogger::log('info', 'Profile updated', ['user_id' => $userId, 'fields' => array_keys($input)]);
$response['success'] = true;
$response['data'] = ['message' => 'Profile updated successfully'];
echo json_encode($response);
}
/**
* Upload avatar
*/
function handleUploadAvatar($userId) {
global $class_database, $response;
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
throw new Exception('No file uploaded or upload error occurred', 400);
}
$file = $_FILES['avatar'];
// Validate file type
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
throw new Exception('Invalid file type. Only JPG, PNG, and GIF are allowed', 400);
}
// Validate file size (max 5MB)
if ($file['size'] > 5 * 1024 * 1024) {
throw new Exception('File too large. Maximum size is 5MB', 400);
}
// Create upload directory if it doesn't exist
$uploadDir = dirname(__FILE__) . '/../f_data/data_userfiles/user_profile/' . $userId . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Generate unique filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'avatar_' . time() . '.' . $extension;
$targetPath = $uploadDir . $filename;
// Move uploaded file
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
throw new Exception('Failed to save uploaded file', 500);
}
// Update user avatar in database
$avatarUrl = '/f_data/data_userfiles/user_profile/' . $userId . '/' . $filename;
$sql = "UPDATE db_users SET usr_avatar = ? WHERE usr_id = ?";
$result = $class_database->execute($sql, [$avatarUrl, $userId]);
if (!$result) {
throw new Exception('Failed to update avatar in database', 500);
}
$response['success'] = true;
$response['data'] = [
'message' => 'Avatar uploaded successfully',
'avatar_url' => $avatarUrl
];
echo json_encode($response);
}
/**
* Get user statistics
*/
function handleGetStats($userId) {
global $class_database, $response;
if (!$userId) {
throw new Exception('Authentication required', 401);
}
$sql = "SELECT
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1) as total_videos,
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND upload_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)) as videos_last_30_days,
(SELECT SUM(file_views) FROM db_videofiles WHERE usr_id = ?) as total_views,
(SELECT COUNT(*) FROM db_subscriptions WHERE channel_id = ?) as total_subscribers,
(SELECT COUNT(*) FROM db_subscriptions WHERE usr_id = ?) as total_subscriptions,
(SELECT COUNT(*) FROM db_comments WHERE usr_id = ?) as total_comments,
(SELECT COUNT(*) FROM db_likes WHERE usr_id = ?) as total_likes_given
FROM dual";
$result = $class_database->execute($sql, [$userId, $userId, $userId, $userId, $userId, $userId, $userId]);
if (!$result || $result->RecordCount() === 0) {
throw new Exception('Failed to fetch statistics', 500);
}
$response['success'] = true;
$response['data'] = $result->fields;
echo json_encode($response);
}
/**
* Get user's videos
*/
function handleGetUserVideos($userId) {
global $class_database, $response;
$targetUserId = isset($_GET['id']) ? (int)$_GET['id'] : $userId;
if (!$targetUserId) {
throw new Exception('User ID required', 400);
}
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 20;
$offset = ($page - 1) * $limit;
// Privacy filter
$privacyWhere = ($targetUserId == $userId) ? "" : "AND privacy = 'public'";
// Get total count
$countSql = "SELECT COUNT(*) FROM db_videofiles WHERE usr_id = ? AND approved = 1 $privacyWhere";
$total = (int)$class_database->singleFieldValue($countSql, [$targetUserId]);
// Get videos
$sql = "SELECT file_key, file_title, file_description, file_duration,
file_views, privacy, upload_date, thumbnail, featured,
(SELECT COUNT(*) FROM db_likes WHERE file_key = db_videofiles.file_key AND like_type = 'like') as like_count,
(SELECT COUNT(*) FROM db_comments WHERE file_key = db_videofiles.file_key) as comment_count
FROM db_videofiles
WHERE usr_id = ? AND approved = 1 $privacyWhere
ORDER BY upload_date DESC
LIMIT ? OFFSET ?";
$videos = $class_database->execute($sql, [$targetUserId, $limit, $offset]);
$response['success'] = true;
$response['data'] = [
'videos' => $videos ? $videos->GetArray() : [],
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
];
echo json_encode($response);
}
/**
* Get user's subscriptions
*/
function handleGetSubscriptions($userId) {
global $class_database, $response;
if (!$userId) {
throw new Exception('Authentication required', 401);
}
$sql = "SELECT u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar,
s.sub_date,
(SELECT COUNT(*) FROM db_videofiles WHERE usr_id = u.usr_id AND approved = 1) as video_count
FROM db_subscriptions s
LEFT JOIN db_users u ON s.channel_id = u.usr_id
WHERE s.usr_id = ?
ORDER BY s.sub_date DESC";
$result = $class_database->execute($sql, [$userId]);
$response['success'] = true;
$response['data'] = $result ? $result->GetArray() : [];
echo json_encode($response);
}
/**
* Get user's subscribers
*/
function handleGetSubscribers($userId) {
global $class_database, $response;
$targetUserId = isset($_GET['id']) ? (int)$_GET['id'] : $userId;
if (!$targetUserId) {
throw new Exception('User ID required', 400);
}
$sql = "SELECT u.usr_id, u.usr_user, u.usr_dname, u.usr_avatar, s.sub_date
FROM db_subscriptions s
LEFT JOIN db_users u ON s.usr_id = u.usr_id
WHERE s.channel_id = ?
ORDER BY s.sub_date DESC";
$result = $class_database->execute($sql, [$targetUserId]);
$response['success'] = true;
$response['data'] = $result ? $result->GetArray() : [];
echo json_encode($response);
}
/**
* Subscribe to a channel
*/
function handleSubscribe($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$channelId = isset($input['channel_id']) ? (int)$input['channel_id'] : null;
if (!$channelId) {
throw new Exception('Channel ID is required', 400);
}
if ($channelId == $userId) {
throw new Exception('Cannot subscribe to yourself', 400);
}
// Check if already subscribed
$checkSql = "SELECT 1 FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$existing = $class_database->execute($checkSql, [$userId, $channelId]);
if ($existing && $existing->RecordCount() > 0) {
throw new Exception('Already subscribed', 400);
}
// Add subscription
$sql = "INSERT INTO db_subscriptions (usr_id, channel_id, sub_date) VALUES (?, ?, NOW())";
$result = $class_database->execute($sql, [$userId, $channelId]);
if (!$result) {
throw new Exception('Failed to subscribe', 500);
}
// Create notification for channel owner
$notifSql = "INSERT INTO db_notifications (usr_id, notif_type, notif_from, notif_date)
VALUES (?, 'subscription', ?, NOW())";
$class_database->execute($notifSql, [$channelId, $userId]);
$response['success'] = true;
$response['data'] = ['message' => 'Subscribed successfully'];
echo json_encode($response);
}
/**
* Unsubscribe from a channel
*/
function handleUnsubscribe($userId) {
global $class_database, $response;
$input = json_decode(file_get_contents('php://input'), true);
$channelId = isset($input['channel_id']) ? (int)$input['channel_id'] : null;
if (!$channelId) {
throw new Exception('Channel ID is required', 400);
}
// Remove subscription
$sql = "DELETE FROM db_subscriptions WHERE usr_id = ? AND channel_id = ?";
$result = $class_database->execute($sql, [$userId, $channelId]);
if (!$result) {
throw new Exception('Failed to unsubscribe', 500);
}
$response['success'] = true;
$response['data'] = ['message' => 'Unsubscribed successfully'];
echo json_encode($response);
}
/**
* Delete user account
*/
function handleDeleteAccount($userId) {
global $class_database, $response;
// This is a destructive operation, so we'll just mark the account as deleted
// rather than actually deleting it
$sql = "UPDATE db_users SET usr_status = 'deleted', usr_email = CONCAT('deleted_', usr_id, '@deleted.com')
WHERE usr_id = ?";
$result = $class_database->execute($sql, [$userId]);
if (!$result) {
throw new Exception('Failed to delete account', 500);
}
// Log out the user
VAuth::logout();
$response['success'] = true;
$response['data'] = ['message' => 'Account deleted successfully'];
echo json_encode($response);
}