frontendPlayer/lib/welcome.blade.php
2026-04-10 09:55:19 +03:30

205 lines
9.6 KiB
PHP

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>آپلودر امن کلاینت‌ساید (Spot Player)</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>body { font-family: Tahoma, system-ui, sans-serif; }</style>
</head>
<body class="bg-gray-900 text-white min-h-screen flex items-center justify-center p-4">
<div class="bg-gray-800 rounded-xl shadow-2xl p-8 w-full max-w-lg border border-gray-700">
<h2 class="text-2xl font-bold mb-6 text-center text-blue-400">آپلودر امن Spot Player</h2>
<div class="space-y-4">
<div>
<label class="block text-sm text-gray-400 mb-1">شناسه دوره (Course ID)</label>
<input type="number" id="courseId" value="1" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-blue-500">
</div>
<div>
<label class="block text-sm text-gray-400 mb-1">عنوان ویدیو</label>
<input type="text" id="videoTitle" placeholder="مثلاً: جلسه اول" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-blue-500">
</div>
<div class="border-2 border-dashed border-gray-600 rounded-lg p-6 text-center hover:border-blue-500 transition cursor-pointer relative">
<input type="file" id="videoFile" accept="video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
<div id="fileLabel" class="text-gray-400">فایل ویدیو را اینجا رها کنید یا کلیک کنید</div>
</div>
<!-- لاگ عملیات -->
<div class="bg-black rounded p-3 h-40 overflow-y-auto text-xs font-mono text-green-400" id="logs">
> آماده برای پردازش...
</div>
<button id="btnStart" onclick="processAndUpload()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded disabled:opacity-50 disabled:cursor-not-allowed transition">
شروع رمزنگاری و آپلود
</button>
</div>
</div>
<script>
const API_BASE_URL = '/api/web'; // آدرس API لاراول
function log(msg) {
const el = document.getElementById('logs');
el.innerHTML += `<div>> ${msg}</div>`;
el.scrollTop = el.scrollHeight;
}
document.getElementById('videoFile').addEventListener('change', function(e) {
if(e.target.files[0]) {
document.getElementById('fileLabel').innerText = e.target.files[0].name;
log(`فایل انتخاب شد: ${e.target.files[0].name} (${(e.target.files[0].size / 1024 / 1024).toFixed(2)} MB)`);
}
});
// تبدیل بافر به رشته هگز
function buf2hex(buffer) {
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
// تبدیل UUID رشته‌ای به آرایه بایت (برای هدر فایل)
function uuidToBytes(uuid) {
const hex = uuid.replace(/-/g, '');
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
return bytes;
}
async function processAndUpload() {
const fileInput = document.getElementById('videoFile');
const courseId = document.getElementById('courseId').value;
const title = document.getElementById('videoTitle').value;
if (!fileInput.files.length) return alert("لطفاً یک فایل انتخاب کنید");
if (!title) return alert("عنوان ویدیو الزامی است");
const file = fileInput.files[0];
const btn = document.getElementById('btnStart');
btn.disabled = true;
try {
log("--- شروع عملیات ---");
// 1. تولید کلید (16 بایت) و نانس (8 بایت) و شناسه محتوا
log("تولید کلیدهای امنیتی...");
const keyBytes = window.crypto.getRandomValues(new Uint8Array(16));
const nonceBytes = window.crypto.getRandomValues(new Uint8Array(8));
const contentId = crypto.randomUUID();
// تبدیل کلید به فرمت هگز برای ارسال به دیتابیس
const contentKeyHex = buf2hex(keyBytes);
log(`شناسه محتوا: ${contentId}`);
// 2. خواندن فایل
log("در حال خواندن فایل در حافظه...");
const fileBuffer = await file.arrayBuffer();
// 3. رمزنگاری (AES-128-CTR)
log("در حال رمزنگاری (AES-128-CTR)...");
// ایمپورت کلید خام برای استفاده در Web Crypto API
const key = await window.crypto.subtle.importKey(
"raw", keyBytes, { name: "AES-CTR" }, false, ["encrypt"]
);
// ساخت کانتر بلاک (16 بایت): 8 بایت نانس + 8 بایت صفر
const counterBlock = new Uint8Array(16);
counterBlock.set(nonceBytes, 0);
// 8 بایت آخر صفر می‌ماند (Big Endian Counter از صفر شروع می‌شود)
const encryptedData = await window.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: counterBlock,
length: 64 // تعداد بیت‌های کانتر (8 بایت = 64 بیت)
},
key,
fileBuffer
);
log("رمزنگاری تکمیل شد.");
// 4. ساخت هدر فایل (Custom Header)
// فرمت: MYPLR1 (6 bytes) + Version (1 byte) + ContentID (16 bytes) + Nonce (8 bytes)
log("در حال بسته‌بندی فایل...");
const magic = new TextEncoder().encode("MYPLR1");
const version = new Uint8Array([1]);
const contentIdBytes = uuidToBytes(contentId);
// محاسبه سایز کل
const headerSize = magic.length + version.length + contentIdBytes.length + nonceBytes.length;
const totalSize = headerSize + encryptedData.byteLength;
const finalBuffer = new Uint8Array(totalSize);
let offset = 0;
finalBuffer.set(magic, offset); offset += magic.length;
finalBuffer.set(version, offset); offset += version.length;
finalBuffer.set(contentIdBytes, offset); offset += contentIdBytes.length;
finalBuffer.set(nonceBytes, offset); offset += nonceBytes.length;
finalBuffer.set(new Uint8Array(encryptedData), offset);
const finalBlob = new Blob([finalBuffer], { type: "application/octet-stream" });
// 5. آپلود به سرور
log("در حال آپلود به سرور...");
const formData = new FormData();
// نام فیلد باید با کنترلر لاراول یکی باشد (video_file)
formData.append('video_file', finalBlob, `${contentId}.spot`);
formData.append('title', title);
formData.append('content_id', contentId);
formData.append('content_key', contentKeyHex); // ارسال کلید به صورت هگز
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_BASE_URL}/courses/${courseId}/videos`, true);
// هدر برای احراز هویت (در صورت نیاز)
// xhr.setRequestHeader('Authorization', 'Bearer ...');
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
log(`پیشرفت آپلود: ${percent}%`);
btn.innerText = `در حال آپلود... ${percent}%`;
}
};
xhr.onload = function() {
if (xhr.status === 200 || xhr.status === 201) {
log("✅ موفقیت! ویدیو با موفقیت ذخیره شد.");
btn.innerText = "آپلود موفقیت‌آمیز بود";
btn.classList.remove('bg-blue-600');
btn.classList.add('bg-green-600');
console.log(JSON.parse(xhr.responseText));
} else {
log("❌ خطا در سمت سرور: " + xhr.responseText);
btn.disabled = false;
btn.innerText = "تلاش مجدد";
}
};
xhr.onerror = function() {
log("❌ خطای شبکه");
btn.disabled = false;
btn.innerText = "تلاش مجدد";
};
xhr.send(formData);
} catch (e) {
log("ERROR: " + e.message);
console.error(e);
btn.disabled = false;
btn.innerText = "شروع مجدد";
}
}
</script>
</body>
</html>