- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
684 lines
23 KiB
PHP
684 lines
23 KiB
PHP
<?php
|
|
/*******************************************************************************************************************
|
|
| Software Name : EasyStream
|
|
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
|
| Software Author : (c) Sami Ahmed
|
|
|*******************************************************************************************************************
|
|
|
|
|
|*******************************************************************************************************************
|
|
| This source file is subject to the EasyStream Proprietary License Agreement.
|
|
|
|
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
|
|*******************************************************************************************************************
|
|
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
|
|*******************************************************************************************************************/
|
|
|
|
defined('_ISVALID') or header('Location: /error');
|
|
|
|
/**
|
|
* RESTful API System for Mobile Apps and Third-Party Integration
|
|
*/
|
|
class VAPI
|
|
{
|
|
private $db;
|
|
private $logger;
|
|
private $rbac;
|
|
private $version = 'v1';
|
|
private $rateLimiter;
|
|
|
|
// API response codes
|
|
const SUCCESS = 200;
|
|
const CREATED = 201;
|
|
const BAD_REQUEST = 400;
|
|
const UNAUTHORIZED = 401;
|
|
const FORBIDDEN = 403;
|
|
const NOT_FOUND = 404;
|
|
const METHOD_NOT_ALLOWED = 405;
|
|
const RATE_LIMITED = 429;
|
|
const SERVER_ERROR = 500;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = VDatabase::getInstance();
|
|
$this->logger = VLogger::getInstance();
|
|
$this->rbac = VRBAC::getInstance();
|
|
$this->rateLimiter = VSecurity::getInstance();
|
|
}
|
|
|
|
/**
|
|
* Handle API request
|
|
* @param string $method HTTP method
|
|
* @param string $endpoint API endpoint
|
|
* @param array $data Request data
|
|
* @param array $headers Request headers
|
|
* @return array API response
|
|
*/
|
|
public function handleRequest($method, $endpoint, $data = [], $headers = [])
|
|
{
|
|
try {
|
|
// Set CORS headers
|
|
$this->setCORSHeaders();
|
|
|
|
// Handle preflight requests
|
|
if ($method === 'OPTIONS') {
|
|
return $this->response(['message' => 'OK'], self::SUCCESS);
|
|
}
|
|
|
|
// Rate limiting
|
|
$clientId = $this->getClientIdentifier($headers);
|
|
if (!$this->checkRateLimit($clientId, $endpoint)) {
|
|
return $this->response(['error' => 'Rate limit exceeded'], self::RATE_LIMITED);
|
|
}
|
|
|
|
// Authentication
|
|
$user = $this->authenticateRequest($headers);
|
|
|
|
// Route request
|
|
$response = $this->routeRequest($method, $endpoint, $data, $user);
|
|
|
|
// Log API request
|
|
$this->logAPIRequest($method, $endpoint, $user['id'] ?? null, $response['status']);
|
|
|
|
return $response;
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('API request failed', [
|
|
'method' => $method,
|
|
'endpoint' => $endpoint,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return $this->response(['error' => 'Internal server error'], self::SERVER_ERROR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Route API request to appropriate handler
|
|
* @param string $method HTTP method
|
|
* @param string $endpoint API endpoint
|
|
* @param array $data Request data
|
|
* @param array|null $user Authenticated user
|
|
* @return array API response
|
|
*/
|
|
private function routeRequest($method, $endpoint, $data, $user)
|
|
{
|
|
$parts = explode('/', trim($endpoint, '/'));
|
|
$resource = $parts[0] ?? '';
|
|
$id = $parts[1] ?? null;
|
|
$action = $parts[2] ?? null;
|
|
|
|
switch ($resource) {
|
|
case 'auth':
|
|
return $this->handleAuth($method, $id, $data);
|
|
|
|
case 'videos':
|
|
return $this->handleVideos($method, $id, $action, $data, $user);
|
|
|
|
case 'users':
|
|
return $this->handleUsers($method, $id, $action, $data, $user);
|
|
|
|
case 'live':
|
|
return $this->handleLiveStreams($method, $id, $action, $data, $user);
|
|
|
|
case 'search':
|
|
return $this->handleSearch($method, $data, $user);
|
|
|
|
case 'upload':
|
|
return $this->handleUpload($method, $data, $user);
|
|
|
|
case 'analytics':
|
|
return $this->handleAnalytics($method, $id, $action, $data, $user);
|
|
|
|
default:
|
|
return $this->response(['error' => 'Endpoint not found'], self::NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle authentication endpoints
|
|
* @param string $method HTTP method
|
|
* @param string $action Action
|
|
* @param array $data Request data
|
|
* @return array API response
|
|
*/
|
|
private function handleAuth($method, $action, $data)
|
|
{
|
|
switch ($action) {
|
|
case 'login':
|
|
if ($method !== 'POST') {
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
return $this->login($data);
|
|
|
|
case 'register':
|
|
if ($method !== 'POST') {
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
return $this->register($data);
|
|
|
|
case 'refresh':
|
|
if ($method !== 'POST') {
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
return $this->refreshToken($data);
|
|
|
|
case 'logout':
|
|
if ($method !== 'POST') {
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
return $this->logout($data);
|
|
|
|
default:
|
|
return $this->response(['error' => 'Auth action not found'], self::NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle video endpoints
|
|
* @param string $method HTTP method
|
|
* @param string $id Video ID
|
|
* @param string $action Action
|
|
* @param array $data Request data
|
|
* @param array|null $user Authenticated user
|
|
* @return array API response
|
|
*/
|
|
private function handleVideos($method, $id, $action, $data, $user)
|
|
{
|
|
switch ($method) {
|
|
case 'GET':
|
|
if ($id) {
|
|
if ($action === 'comments') {
|
|
return $this->getVideoComments($id, $data);
|
|
} elseif ($action === 'related') {
|
|
return $this->getRelatedVideos($id, $data);
|
|
} else {
|
|
return $this->getVideo($id, $user);
|
|
}
|
|
} else {
|
|
return $this->getVideos($data, $user);
|
|
}
|
|
|
|
case 'POST':
|
|
if ($id && $action === 'like') {
|
|
return $this->likeVideo($id, $user);
|
|
} elseif ($id && $action === 'comment') {
|
|
return $this->commentVideo($id, $data, $user);
|
|
} else {
|
|
return $this->createVideo($data, $user);
|
|
}
|
|
|
|
case 'PUT':
|
|
if ($id) {
|
|
return $this->updateVideo($id, $data, $user);
|
|
}
|
|
break;
|
|
|
|
case 'DELETE':
|
|
if ($id) {
|
|
return $this->deleteVideo($id, $user);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
|
|
/**
|
|
* Handle user endpoints
|
|
* @param string $method HTTP method
|
|
* @param string $id User ID
|
|
* @param string $action Action
|
|
* @param array $data Request data
|
|
* @param array|null $user Authenticated user
|
|
* @return array API response
|
|
*/
|
|
private function handleUsers($method, $id, $action, $data, $user)
|
|
{
|
|
switch ($method) {
|
|
case 'GET':
|
|
if ($id) {
|
|
if ($action === 'videos') {
|
|
return $this->getUserVideos($id, $data);
|
|
} elseif ($action === 'followers') {
|
|
return $this->getUserFollowers($id, $data);
|
|
} elseif ($action === 'following') {
|
|
return $this->getUserFollowing($id, $data);
|
|
} else {
|
|
return $this->getUser($id, $user);
|
|
}
|
|
} else {
|
|
return $this->getUsers($data, $user);
|
|
}
|
|
|
|
case 'POST':
|
|
if ($id && $action === 'follow') {
|
|
return $this->followUser($id, $user);
|
|
}
|
|
break;
|
|
|
|
case 'PUT':
|
|
if ($id === 'me' || ($user && $id == $user['id'])) {
|
|
return $this->updateProfile($data, $user);
|
|
}
|
|
break;
|
|
|
|
case 'DELETE':
|
|
if ($id && $action === 'follow') {
|
|
return $this->unfollowUser($id, $user);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
|
|
/**
|
|
* Handle live stream endpoints
|
|
* @param string $method HTTP method
|
|
* @param string $id Stream ID
|
|
* @param string $action Action
|
|
* @param array $data Request data
|
|
* @param array|null $user Authenticated user
|
|
* @return array API response
|
|
*/
|
|
private function handleLiveStreams($method, $id, $action, $data, $user)
|
|
{
|
|
if (!$user) {
|
|
return $this->response(['error' => 'Authentication required'], self::UNAUTHORIZED);
|
|
}
|
|
|
|
switch ($method) {
|
|
case 'GET':
|
|
if ($id) {
|
|
return $this->getLiveStream($id, $user);
|
|
} else {
|
|
return $this->getLiveStreams($data, $user);
|
|
}
|
|
|
|
case 'POST':
|
|
if ($id && $action === 'start') {
|
|
return $this->startLiveStream($id, $user);
|
|
} elseif ($id && $action === 'stop') {
|
|
return $this->stopLiveStream($id, $user);
|
|
} else {
|
|
return $this->createLiveStream($data, $user);
|
|
}
|
|
|
|
case 'PUT':
|
|
if ($id) {
|
|
return $this->updateLiveStream($id, $data, $user);
|
|
}
|
|
break;
|
|
|
|
case 'DELETE':
|
|
if ($id) {
|
|
return $this->deleteLiveStream($id, $user);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $this->response(['error' => 'Method not allowed'], self::METHOD_NOT_ALLOWED);
|
|
}
|
|
|
|
/**
|
|
* Authenticate API request
|
|
* @param array $headers Request headers
|
|
* @return array|null User data or null if not authenticated
|
|
*/
|
|
private function authenticateRequest($headers)
|
|
{
|
|
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
|
|
|
|
if (empty($authHeader)) {
|
|
return null;
|
|
}
|
|
|
|
// Support Bearer token format
|
|
if (strpos($authHeader, 'Bearer ') === 0) {
|
|
$token = substr($authHeader, 7);
|
|
return $this->validateJWTToken($token);
|
|
}
|
|
|
|
// Support API key format
|
|
if (strpos($authHeader, 'ApiKey ') === 0) {
|
|
$apiKey = substr($authHeader, 7);
|
|
return $this->validateAPIKey($apiKey);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate JWT token
|
|
* @param string $token JWT token
|
|
* @return array|null User data or null if invalid
|
|
*/
|
|
private function validateJWTToken($token)
|
|
{
|
|
try {
|
|
// Simple JWT validation (in production, use a proper JWT library)
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 3) {
|
|
return null;
|
|
}
|
|
|
|
$payload = json_decode(base64_decode($parts[1]), true);
|
|
|
|
if (!$payload || !isset($payload['user_id']) || $payload['exp'] < time()) {
|
|
return null;
|
|
}
|
|
|
|
// Get user from database
|
|
$query = "SELECT usr_id, usr_user, usr_email, usr_dname FROM db_accountuser WHERE usr_id = ? AND usr_status = 'active'";
|
|
$result = $this->db->doQuery($query, [$payload['user_id']]);
|
|
$user = $this->db->doFetch($result);
|
|
|
|
if ($user) {
|
|
return [
|
|
'id' => $user['usr_id'],
|
|
'username' => $user['usr_user'],
|
|
'email' => $user['usr_email'],
|
|
'display_name' => $user['usr_dname']
|
|
];
|
|
}
|
|
|
|
return null;
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('JWT validation failed', ['error' => $e->getMessage()]);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate API key
|
|
* @param string $apiKey API key
|
|
* @return array|null User data or null if invalid
|
|
*/
|
|
private function validateAPIKey($apiKey)
|
|
{
|
|
try {
|
|
$query = "SELECT ak.user_id, ak.permissions, au.usr_user, au.usr_email, au.usr_dname
|
|
FROM db_api_keys ak
|
|
JOIN db_accountuser au ON ak.user_id = au.usr_id
|
|
WHERE ak.api_key = ? AND ak.status = 'active' AND ak.expires_at > NOW()";
|
|
$result = $this->db->doQuery($query, [$apiKey]);
|
|
$keyData = $this->db->doFetch($result);
|
|
|
|
if ($keyData) {
|
|
return [
|
|
'id' => $keyData['user_id'],
|
|
'username' => $keyData['usr_user'],
|
|
'email' => $keyData['usr_email'],
|
|
'display_name' => $keyData['usr_dname'],
|
|
'api_permissions' => json_decode($keyData['permissions'], true) ?: []
|
|
];
|
|
}
|
|
|
|
return null;
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('API key validation failed', ['error' => $e->getMessage()]);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate JWT token
|
|
* @param array $user User data
|
|
* @return string JWT token
|
|
*/
|
|
private function generateJWTToken($user)
|
|
{
|
|
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
|
$payload = json_encode([
|
|
'user_id' => $user['id'],
|
|
'username' => $user['username'],
|
|
'iat' => time(),
|
|
'exp' => time() + (24 * 60 * 60) // 24 hours
|
|
]);
|
|
|
|
$headerEncoded = base64_encode($header);
|
|
$payloadEncoded = base64_encode($payload);
|
|
|
|
$signature = hash_hmac('sha256', $headerEncoded . '.' . $payloadEncoded, 'your-secret-key', true);
|
|
$signatureEncoded = base64_encode($signature);
|
|
|
|
return $headerEncoded . '.' . $payloadEncoded . '.' . $signatureEncoded;
|
|
}
|
|
|
|
/**
|
|
* Login user
|
|
* @param array $data Login data
|
|
* @return array API response
|
|
*/
|
|
private function login($data)
|
|
{
|
|
$username = $data['username'] ?? '';
|
|
$password = $data['password'] ?? '';
|
|
|
|
if (empty($username) || empty($password)) {
|
|
return $this->response(['error' => 'Username and password required'], self::BAD_REQUEST);
|
|
}
|
|
|
|
// Validate credentials
|
|
$query = "SELECT usr_id, usr_user, usr_email, usr_dname, usr_password
|
|
FROM db_accountuser
|
|
WHERE (usr_user = ? OR usr_email = ?) AND usr_status = 'active'";
|
|
$result = $this->db->doQuery($query, [$username, $username]);
|
|
$user = $this->db->doFetch($result);
|
|
|
|
if (!$user || !password_verify($password, $user['usr_password'])) {
|
|
return $this->response(['error' => 'Invalid credentials'], self::UNAUTHORIZED);
|
|
}
|
|
|
|
// Generate token
|
|
$userData = [
|
|
'id' => $user['usr_id'],
|
|
'username' => $user['usr_user'],
|
|
'email' => $user['usr_email'],
|
|
'display_name' => $user['usr_dname']
|
|
];
|
|
|
|
$token = $this->generateJWTToken($userData);
|
|
|
|
// Update last login
|
|
$this->db->doUpdate('db_accountuser', 'usr_id', [
|
|
'usr_lastlogin' => date('Y-m-d H:i:s')
|
|
], $user['usr_id']);
|
|
|
|
return $this->response([
|
|
'token' => $token,
|
|
'user' => $userData,
|
|
'expires_in' => 86400 // 24 hours
|
|
], self::SUCCESS);
|
|
}
|
|
|
|
/**
|
|
* Get videos
|
|
* @param array $params Query parameters
|
|
* @param array|null $user Authenticated user
|
|
* @return array API response
|
|
*/
|
|
private function getVideos($params, $user)
|
|
{
|
|
$page = max(1, (int)($params['page'] ?? 1));
|
|
$limit = min(50, max(1, (int)($params['limit'] ?? 20)));
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
$category = $params['category'] ?? '';
|
|
$sort = $params['sort'] ?? 'recent';
|
|
|
|
$where = ["vf.file_type = 'video'", "vf.privacy = 'public'"];
|
|
$queryParams = [];
|
|
|
|
if ($category) {
|
|
$where[] = "vf.file_category = ?";
|
|
$queryParams[] = $category;
|
|
}
|
|
|
|
$orderBy = match($sort) {
|
|
'popular' => 'vf.file_views DESC',
|
|
'recent' => 'vf.upload_date DESC',
|
|
'rating' => 'vf.file_rating DESC',
|
|
default => 'vf.upload_date DESC'
|
|
};
|
|
|
|
$whereClause = implode(' AND ', $where);
|
|
|
|
$query = "SELECT vf.file_key, vf.file_title, vf.file_description, vf.file_views,
|
|
vf.file_rating, vf.upload_date, vf.file_duration, vf.file_size,
|
|
au.usr_user, au.usr_dname
|
|
FROM db_videofiles vf
|
|
JOIN db_accountuser au ON vf.usr_id = au.usr_id
|
|
WHERE {$whereClause}
|
|
ORDER BY {$orderBy}
|
|
LIMIT {$limit} OFFSET {$offset}";
|
|
|
|
$result = $this->db->doQuery($query, $queryParams);
|
|
|
|
$videos = [];
|
|
while ($row = $this->db->doFetch($result)) {
|
|
$videos[] = [
|
|
'id' => $row['file_key'],
|
|
'title' => $row['file_title'],
|
|
'description' => $row['file_description'],
|
|
'views' => (int)$row['file_views'],
|
|
'rating' => (float)$row['file_rating'],
|
|
'duration' => (int)$row['file_duration'],
|
|
'size' => (int)$row['file_size'],
|
|
'uploaded_at' => $row['upload_date'],
|
|
'uploader' => [
|
|
'username' => $row['usr_user'],
|
|
'display_name' => $row['usr_dname']
|
|
],
|
|
'thumbnail_url' => "/thumbnails/{$row['file_key']}_medium.jpg",
|
|
'video_url' => "/watch/{$row['file_key']}"
|
|
];
|
|
}
|
|
|
|
return $this->response([
|
|
'videos' => $videos,
|
|
'pagination' => [
|
|
'page' => $page,
|
|
'limit' => $limit,
|
|
'total' => $this->getVideoCount($where, $queryParams)
|
|
]
|
|
], self::SUCCESS);
|
|
}
|
|
|
|
/**
|
|
* Create API response
|
|
* @param array $data Response data
|
|
* @param int $status HTTP status code
|
|
* @return array API response
|
|
*/
|
|
private function response($data, $status = self::SUCCESS)
|
|
{
|
|
return [
|
|
'status' => $status,
|
|
'data' => $data,
|
|
'timestamp' => time(),
|
|
'version' => $this->version
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Set CORS headers
|
|
*/
|
|
private function setCORSHeaders()
|
|
{
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
|
header('Access-Control-Max-Age: 86400');
|
|
}
|
|
|
|
/**
|
|
* Check rate limit
|
|
* @param string $clientId Client identifier
|
|
* @param string $endpoint Endpoint
|
|
* @return bool True if within limits
|
|
*/
|
|
private function checkRateLimit($clientId, $endpoint)
|
|
{
|
|
// Different limits for different endpoints
|
|
$limits = [
|
|
'auth' => ['requests' => 10, 'window' => 300], // 10 requests per 5 minutes
|
|
'upload' => ['requests' => 5, 'window' => 3600], // 5 uploads per hour
|
|
'default' => ['requests' => 100, 'window' => 3600] // 100 requests per hour
|
|
];
|
|
|
|
$endpointType = explode('/', $endpoint)[0] ?? 'default';
|
|
$limit = $limits[$endpointType] ?? $limits['default'];
|
|
|
|
return $this->rateLimiter->checkRateLimit(
|
|
"api_{$clientId}_{$endpointType}",
|
|
$limit['requests'],
|
|
$limit['window'],
|
|
"api_{$endpointType}"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get client identifier
|
|
* @param array $headers Request headers
|
|
* @return string Client identifier
|
|
*/
|
|
private function getClientIdentifier($headers)
|
|
{
|
|
// Use API key if available, otherwise IP address
|
|
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
|
|
|
|
if (strpos($authHeader, 'ApiKey ') === 0) {
|
|
return 'key_' . substr($authHeader, 7, 10);
|
|
}
|
|
|
|
return 'ip_' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
|
}
|
|
|
|
/**
|
|
* Log API request
|
|
* @param string $method HTTP method
|
|
* @param string $endpoint Endpoint
|
|
* @param int|null $userId User ID
|
|
* @param int $status Response status
|
|
*/
|
|
private function logAPIRequest($method, $endpoint, $userId, $status)
|
|
{
|
|
try {
|
|
$logData = [
|
|
'method' => $method,
|
|
'endpoint' => $endpoint,
|
|
'user_id' => $userId,
|
|
'status' => $status,
|
|
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->db->doInsert('db_api_logs', $logData);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to log API request', [
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get video count for pagination
|
|
* @param array $where Where conditions
|
|
* @param array $params Query parameters
|
|
* @return int Total count
|
|
*/
|
|
private function getVideoCount($where, $params)
|
|
{
|
|
$whereClause = implode(' AND ', $where);
|
|
$query = "SELECT COUNT(*) as total FROM db_videofiles vf WHERE {$whereClause}";
|
|
$result = $this->db->doQuery($query, $params);
|
|
$row = $this->db->doFetch($result);
|
|
|
|
return (int)($row['total'] ?? 0);
|
|
}
|
|
} |