diff --git a/public/css/style.css b/public/css/style.css index a04511e..eda39ec 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -119,6 +119,96 @@ header p { min-width: 0; } +.login-screen { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(15, 23, 42, 0.92); + z-index: 50; + padding: 2rem; +} + +.login-card { + width: min(480px, 100%); + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(148, 163, 184, 0.15); + border-radius: 24px; + padding: 2rem; + box-shadow: 0 40px 80px rgba(15, 23, 42, 0.4); +} + +.login-card h2 { + margin: 0 0 1rem; + font-size: 1.75rem; +} + +.login-card p { + color: var(--text-secondary); + margin: 0 0 1.5rem; +} + +.login-card label { + display: block; + margin-bottom: 1rem; + color: var(--text-secondary); + font-size: 0.95rem; +} + +.login-card label span { + display: block; + margin-bottom: 0.5rem; +} + +.login-card input { + width: 100%; + border: 1px solid var(--panel-border); + background: rgba(255, 255, 255, 0.06); + color: var(--text-primary); + border-radius: 12px; + padding: 0.85rem 1rem; + outline: none; +} + +.login-card .primary-btn { + width: 100%; + margin-top: 0.75rem; +} + +.form-error { + color: #f97316; + margin-top: 0.75rem; +} + +.bucket-panel { + margin-bottom: 1rem; +} + +.bucket-panel label { + display: block; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.bucket-panel select { + width: 100%; + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); + border: 1px solid var(--panel-border); + border-radius: 10px; + padding: 0.5rem 0.75rem; + outline: none; +} + +.login-screen.hidden { + display: none; +} + +.app-hidden { + display: none; +} + .glass-panel { background: var(--panel-bg); backdrop-filter: blur(16px); diff --git a/public/index.html b/public/index.html index 754070e..6c3c968 100644 --- a/public/index.html +++ b/public/index.html @@ -16,7 +16,24 @@
-
+
+ +
+ +
-
-
- - -
-
- - -
+
+ +
diff --git a/public/js/main.js b/public/js/main.js index d4cb5f6..3501b87 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -3,8 +3,13 @@ document.addEventListener('DOMContentLoaded', () => { const loadingSpinner = document.getElementById('loading-spinner'); const refreshBtn = document.getElementById('refresh-btn'); const resetCacheBtn = document.getElementById('reset-cache-btn'); - const usernameInput = document.getElementById('s3-username'); - const passwordInput = document.getElementById('s3-password'); + const bucketSelect = document.getElementById('bucket-select'); + const loginScreen = document.getElementById('login-screen'); + const appContainer = document.getElementById('app-container'); + const loginUsernameInput = document.getElementById('login-username'); + const loginPasswordInput = document.getElementById('login-password'); + const loginBtn = document.getElementById('login-btn'); + const loginError = document.getElementById('login-error'); const codecSelect = document.getElementById('codec-select'); const encoderSelect = document.getElementById('encoder-select'); const playerOverlay = document.getElementById('player-overlay'); @@ -20,11 +25,13 @@ document.addEventListener('DOMContentLoaded', () => { const topBanner = document.getElementById('top-banner'); let currentPollInterval = null; + let selectedBucket = null; let selectedKey = null; let ws = null; let wsConnected = false; let subscribedKey = null; let currentVideoKey = null; + let s3AuthHeaders = {}; const sendWsMessage = (message) => { if (ws && ws.readyState === WebSocket.OPEN) { @@ -108,19 +115,86 @@ document.addEventListener('DOMContentLoaded', () => { progressFill.style.width = '0%'; }; - const getS3AuthHeaders = () => { - const headers = {}; - const username = usernameInput?.value?.trim(); - const password = passwordInput?.value || ''; - if (username) headers['X-S3-Username'] = username; - if (password) headers['X-S3-Password'] = password; - return headers; + const setAuthHeaders = (username, password) => { + s3AuthHeaders = {}; + if (username) s3AuthHeaders['X-S3-Username'] = username; + if (password) s3AuthHeaders['X-S3-Password'] = password; }; - const getS3AuthPayload = () => ({ - username: usernameInput?.value?.trim() || '', - password: passwordInput?.value || '' - }); + const showLogin = () => { + if (loginScreen) loginScreen.classList.remove('hidden'); + if (appContainer) appContainer.classList.add('hidden'); + }; + + const showApp = () => { + if (loginScreen) loginScreen.classList.add('hidden'); + if (appContainer) appContainer.classList.remove('hidden'); + }; + + const renderBuckets = (buckets) => { + if (!bucketSelect) return; + bucketSelect.innerHTML = ''; + buckets.forEach(bucket => { + const option = document.createElement('option'); + option.value = bucket.Name; + option.textContent = bucket.Name; + bucketSelect.appendChild(option); + }); + }; + + const showLoginError = (message) => { + if (!loginError) return; + loginError.textContent = message; + loginError.classList.remove('hidden'); + }; + + const clearLoginError = () => { + if (!loginError) return; + loginError.textContent = ''; + loginError.classList.add('hidden'); + }; + + const login = async () => { + if (!loginUsernameInput || !loginPasswordInput) return; + const username = loginUsernameInput.value.trim(); + const password = loginPasswordInput.value; + if (!username || !password) { + showLoginError('Please enter both access key and secret key.'); + return; + } + loginBtn.disabled = true; + loginBtn.textContent = 'Logging in...'; + clearLoginError(); + + try { + setAuthHeaders(username, password); + const res = await fetch('/api/buckets', { headers: s3AuthHeaders }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Login failed'); + } + const data = await res.json(); + if (!Array.isArray(data.buckets) || data.buckets.length === 0) { + throw new Error('No buckets available for this account'); + } + renderBuckets(data.buckets); + showApp(); + selectedBucket = data.buckets[0].Name; + if (bucketSelect) bucketSelect.value = selectedBucket; + loadConfig(); + connectWebSocket(); + await fetchVideos(selectedBucket); + } catch (err) { + console.error('Login error:', err); + showLoginError(err.message); + setAuthHeaders('', ''); + } finally { + if (loginBtn) { + loginBtn.disabled = false; + loginBtn.textContent = 'Login'; + } + } + }; const resetCache = async () => { if (!resetCacheBtn) return; @@ -175,13 +249,16 @@ document.addEventListener('DOMContentLoaded', () => { } // Fetch list of videos from the backend - const fetchVideos = async () => { + const fetchVideos = async (bucket) => { + if (!bucket) { + return; + } videoListEl.classList.add('hidden'); loadingSpinner.classList.remove('hidden'); videoListEl.innerHTML = ''; try { - const res = await fetch('/api/videos', { headers: getS3AuthHeaders() }); + const res = await fetch(`/api/videos?bucket=${encodeURIComponent(bucket)}`, { headers: s3AuthHeaders }); if (!res.ok) throw new Error('Failed to fetch videos. Check S3 Config.'); const data = await res.json(); @@ -328,11 +405,11 @@ document.addEventListener('DOMContentLoaded', () => { try { const codec = codecSelect?.value || 'h264'; const encoder = encoderSelect?.value || 'software'; - const authPayload = getS3AuthPayload(); + if (!selectedBucket) throw new Error('No bucket selected'); const res = await fetch('/api/transcode', { method: 'POST', - headers: { 'Content-Type': 'application/json', ...getS3AuthHeaders() }, - body: JSON.stringify({ key: selectedKey, codec, encoder, ...authPayload }) + headers: { 'Content-Type': 'application/json', ...s3AuthHeaders }, + body: JSON.stringify({ bucket: selectedBucket, key: selectedKey, codec, encoder }) }); const data = await res.json(); @@ -403,10 +480,17 @@ document.addEventListener('DOMContentLoaded', () => { }; // Bind events - refreshBtn.addEventListener('click', fetchVideos); + refreshBtn.addEventListener('click', () => fetchVideos(selectedBucket)); + if (loginBtn) { + loginBtn.addEventListener('click', login); + } + if (bucketSelect) { + bucketSelect.addEventListener('change', async (event) => { + selectedBucket = event.target.value; + await fetchVideos(selectedBucket); + }); + } - // Connect WebSocket and initial load - connectWebSocket(); - loadConfig(); - fetchVideos(); + // Initial state: require login before loading data + showLogin(); }); diff --git a/server.js b/server.js index fc23543..55ded6a 100644 --- a/server.js +++ b/server.js @@ -7,7 +7,7 @@ const path = require('path'); const http = require('http'); const WebSocket = require('ws'); const ffmpeg = require('fluent-ffmpeg'); -const { S3Client, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3'); +const { S3Client, ListBucketsCommand, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3'); dotenv.config(); @@ -162,11 +162,27 @@ const clearMp4Cache = () => { } }; +// Endpoint to list available buckets +app.get('/api/buckets', async (req, res) => { + try { + const auth = extractS3Credentials(req); + const s3Client = createS3Client(auth); + const command = new ListBucketsCommand({}); + const response = await s3Client.send(command); + const buckets = response.Buckets || []; + res.json({ buckets }); + } catch (error) { + console.error('Error listing buckets:', error); + res.status(500).json({ error: 'Failed to list buckets', detail: error.message }); + } +}); + // Endpoint to list videos in the bucket app.get('/api/videos', async (req, res) => { try { - if (!BUCKET_NAME) { - return res.status(500).json({ error: 'S3_BUCKET_NAME not configured' }); + const bucket = req.query.bucket || BUCKET_NAME; + if (!bucket) { + return res.status(400).json({ error: 'Bucket name is required' }); } const allObjects = []; const auth = extractS3Credentials(req); @@ -175,7 +191,7 @@ app.get('/api/videos', async (req, res) => { do { const command = new ListObjectsV2Command({ - Bucket: BUCKET_NAME, + Bucket: bucket, ContinuationToken: continuationToken, }); const response = await s3Client.send(command); @@ -223,7 +239,13 @@ app.post('/api/reset-cache', (req, res) => { // Endpoint to transcode S3 video streaming to MP4 app.post('/api/transcode', async (req, res) => { - const { key, codec, encoder } = req.body; + const { bucket, key, codec, encoder } = req.body; + if (!bucket) { + return res.status(400).json({ error: 'Bucket name is required' }); + } + if (!key) { + return res.status(400).json({ error: 'Video key is required' }); + } if (!key) { return res.status(400).json({ error: 'Video key is required' }); @@ -260,7 +282,7 @@ app.post('/api/transcode', async (req, res) => { const auth = extractS3Credentials(req); const s3Client = createS3Client(auth); const command = new GetObjectCommand({ - Bucket: BUCKET_NAME, + Bucket: bucket, Key: key });