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
});