添加退出登录按钮

This commit is contained in:
CN-JS-HuiBai
2026-04-03 22:45:06 +08:00
parent 14695fcfd4
commit 1dd26e634f
4 changed files with 192 additions and 11 deletions

View File

@@ -240,23 +240,61 @@ header p {
} }
.top-banner { .top-banner {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--panel-border); border: 1px solid var(--panel-border);
border-radius: 16px; border-radius: 16px;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: var(--text-primary); color: var(--text-primary);
font-size: 1.25rem;
font-weight: 700;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: space-between;
gap: 1rem;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
} }
.top-banner.hidden { .top-banner.hidden {
display: none; display: none;
} }
.top-banner__title {
font-size: 1.25rem;
font-weight: 700;
min-width: 0;
}
.top-banner__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
flex-wrap: wrap;
}
.user-chip {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.65rem 0.9rem;
border-radius: 999px;
background: rgba(37, 99, 235, 0.08);
border: 1px solid rgba(37, 99, 235, 0.16);
color: var(--text-primary);
line-height: 1;
}
.user-chip__label {
color: var(--text-secondary);
font-size: 0.82rem;
font-weight: 600;
}
.user-chip__name {
font-size: 0.92rem;
font-weight: 700;
}
.section-header { .section-header {
display: block; display: block;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
@@ -291,6 +329,16 @@ header p {
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
} }
.action-btn-secondary {
background: #0f172a;
color: #ffffff;
}
.action-btn-secondary:hover {
background: #1e293b;
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.16);
}
.action-btn:hover { .action-btn:hover {
background: #1d4ed8; background: #1d4ed8;
transform: translateY(-1px); transform: translateY(-1px);
@@ -1044,6 +1092,16 @@ header p {
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 900px) { @media (max-width: 900px) {
.top-banner {
align-items: flex-start;
flex-direction: column;
}
.top-banner__actions {
width: 100%;
justify-content: space-between;
}
.dashboard { .dashboard {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@@ -34,7 +34,16 @@
</div> </div>
<div class="container hidden" id="app-container"> <div class="container hidden" id="app-container">
<div id="top-banner" class="top-banner hidden"></div> <div id="top-banner" class="top-banner hidden">
<div class="top-banner__title" id="top-banner-title"></div>
<div class="top-banner__actions">
<div class="user-chip" id="user-chip">
<span class="user-chip__label">当前用户</span>
<span class="user-chip__name" id="current-username">-</span>
</div>
<button id="logout-btn" class="action-btn action-btn-secondary" type="button">退出登录</button>
</div>
</div>
<main class="dashboard"> <main class="dashboard">
<section class="video-list-section glass-panel"> <section class="video-list-section glass-panel">

View File

@@ -22,6 +22,9 @@ document.addEventListener('DOMContentLoaded', () => {
const stopTranscodeBtn = document.getElementById('stop-transcode-btn'); const stopTranscodeBtn = document.getElementById('stop-transcode-btn');
const playBtn = document.getElementById('play-btn'); const playBtn = document.getElementById('play-btn');
const topBanner = document.getElementById('top-banner'); const topBanner = document.getElementById('top-banner');
const topBannerTitle = document.getElementById('top-banner-title');
const currentUsername = document.getElementById('current-username');
const logoutBtn = document.getElementById('logout-btn');
const customControls = document.getElementById('custom-controls'); const customControls = document.getElementById('custom-controls');
const controlPlayToggle = document.getElementById('control-play-toggle'); const controlPlayToggle = document.getElementById('control-play-toggle');
const controlMuteToggle = document.getElementById('control-mute-toggle'); const controlMuteToggle = document.getElementById('control-mute-toggle');
@@ -71,6 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
let currentVideoKey = null; let currentVideoKey = null;
let s3Username = ''; let s3Username = '';
let s3Password = ''; let s3Password = '';
let authenticatedUsername = '';
let s3AuthHeaders = {}; let s3AuthHeaders = {};
const LEGACY_AUTH_STORAGE_KEY = 'media-coding-web:s3-auth'; const LEGACY_AUTH_STORAGE_KEY = 'media-coding-web:s3-auth';
const SAVED_BUCKET_STORAGE_KEY = 'media-coding-web:selected-bucket'; const SAVED_BUCKET_STORAGE_KEY = 'media-coding-web:selected-bucket';
@@ -723,13 +727,13 @@ document.addEventListener('DOMContentLoaded', () => {
// --- End custom seek bar --- // --- End custom seek bar ---
const loadConfig = async () => { const loadConfig = async () => {
if (!topBanner) return; if (!topBanner || !topBannerTitle) return;
try { try {
const res = await fetch('/api/config'); const res = await fetch('/api/config');
if (!res.ok) throw new Error('Failed to load config'); if (!res.ok) throw new Error('Failed to load config');
const data = await res.json(); const data = await res.json();
const title = data.title || 'S3 Media Transcoder'; const title = data.title || 'S3 Media Transcoder';
topBanner.textContent = title; topBannerTitle.textContent = title;
topBanner.classList.remove('hidden'); topBanner.classList.remove('hidden');
document.title = title; document.title = title;
populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_rkmpp'); populateSelect(encoderSelect, data.videoEncoders || [], data.defaultVideoEncoder || 'h264_rkmpp');
@@ -800,11 +804,20 @@ document.addEventListener('DOMContentLoaded', () => {
const showLogin = () => { const showLogin = () => {
if (loginScreen) loginScreen.classList.remove('hidden'); if (loginScreen) loginScreen.classList.remove('hidden');
if (appContainer) appContainer.classList.add('hidden'); if (appContainer) appContainer.classList.add('hidden');
if (topBanner) topBanner.classList.add('hidden');
}; };
const showApp = () => { const showApp = () => {
if (loginScreen) loginScreen.classList.add('hidden'); if (loginScreen) loginScreen.classList.add('hidden');
if (appContainer) appContainer.classList.remove('hidden'); if (appContainer) appContainer.classList.remove('hidden');
if (topBanner) topBanner.classList.remove('hidden');
};
const updateAuthenticatedUser = (username) => {
authenticatedUsername = typeof username === 'string' ? username.trim() : '';
if (currentUsername) {
currentUsername.textContent = authenticatedUsername || '-';
}
}; };
const renderBuckets = (buckets) => { const renderBuckets = (buckets) => {
@@ -830,11 +843,62 @@ document.addEventListener('DOMContentLoaded', () => {
loginError.classList.add('hidden'); loginError.classList.add('hidden');
}; };
const applyAuthenticatedState = async (buckets) => { const resetAuthenticatedState = () => {
stopPolling();
selectedBucket = null;
selectedKey = null;
currentVideoKey = null;
subscribedKey = null;
setAuthHeaders('', '');
saveSelectedBucket('');
updateAuthenticatedUser('');
clearLoginError();
if (bucketSelect) {
bucketSelect.innerHTML = '<option value="" disabled selected>Choose a bucket</option>';
}
if (videoListEl) {
videoListEl.innerHTML = '';
videoListEl.classList.add('hidden');
}
if (loadingSpinner) {
loadingSpinner.classList.remove('hidden');
loadingSpinner.innerHTML = `
<div class="spinner"></div>
<p>Fetching S3 Objects...</p>
`;
}
if (nowPlaying) nowPlaying.classList.add('hidden');
if (currentVideoTitle) currentVideoTitle.textContent = 'video.mp4';
if (loginPasswordInput) loginPasswordInput.value = '';
if (transcodingOverlay) transcodingOverlay.classList.add('hidden');
if (playerOverlay) playerOverlay.classList.remove('hidden');
if (videoPlayer) {
videoPlayer.pause();
videoPlayer.removeAttribute('src');
videoPlayer.load();
videoPlayer.classList.add('hidden');
}
isStreamActive = false;
videoDuration = 0;
seekOffset = 0;
if (seekCurrentTime) seekCurrentTime.textContent = formatTime(0);
if (seekTotalTime) seekTotalTime.textContent = formatTime(0);
hideSeekBar();
hideCustomControls();
resetPhases();
resetSubtitleTracks();
setPlaybackStatus('Paused', 'paused');
updatePlayControls();
updateVolumeControls();
};
const applyAuthenticatedState = async (buckets, username = '') => {
if (!Array.isArray(buckets) || buckets.length === 0) { if (!Array.isArray(buckets) || buckets.length === 0) {
throw new Error('No buckets available for this account'); throw new Error('No buckets available for this account');
} }
updateAuthenticatedUser(username);
renderBuckets(buckets); renderBuckets(buckets);
showApp(); showApp();
const savedBucket = loadSelectedBucket(); const savedBucket = loadSelectedBucket();
@@ -851,6 +915,33 @@ document.addEventListener('DOMContentLoaded', () => {
await fetchVideos(selectedBucket); await fetchVideos(selectedBucket);
}; };
const logout = async () => {
if (logoutBtn) {
logoutBtn.disabled = true;
logoutBtn.textContent = '退出中...';
}
try {
const res = await fetch('/api/logout', { method: 'POST' });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Logout failed');
}
} catch (error) {
console.error('Logout failed:', error);
alert(`退出登录失败: ${error.message}`);
return;
} finally {
if (logoutBtn) {
logoutBtn.disabled = false;
logoutBtn.textContent = '退出登录';
}
}
resetAuthenticatedState();
showLogin();
};
const login = async (options = {}) => { const login = async (options = {}) => {
if (!loginUsernameInput || !loginPasswordInput) return false; if (!loginUsernameInput || !loginPasswordInput) return false;
const username = typeof options.username === 'string' ? options.username.trim() : loginUsernameInput.value.trim(); const username = typeof options.username === 'string' ? options.username.trim() : loginUsernameInput.value.trim();
@@ -876,7 +967,7 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await res.json(); const data = await res.json();
if (loginUsernameInput) loginUsernameInput.value = username; if (loginUsernameInput) loginUsernameInput.value = username;
if (loginPasswordInput) loginPasswordInput.value = ''; if (loginPasswordInput) loginPasswordInput.value = '';
await applyAuthenticatedState(data.buckets); await applyAuthenticatedState(data.buckets, data.username || username);
return true; return true;
} catch (err) { } catch (err) {
console.error('Login error:', err); console.error('Login error:', err);
@@ -900,7 +991,7 @@ document.addEventListener('DOMContentLoaded', () => {
return false; return false;
} }
const data = await res.json(); const data = await res.json();
await applyAuthenticatedState(data.buckets || []); await applyAuthenticatedState(data.buckets || [], data.username || '');
return true; return true;
} catch (error) { } catch (error) {
console.error('Session restore failed:', error); console.error('Session restore failed:', error);
@@ -1344,6 +1435,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (loginBtn) { if (loginBtn) {
loginBtn.addEventListener('click', login); loginBtn.addEventListener('click', login);
} }
if (logoutBtn) {
logoutBtn.addEventListener('click', logout);
}
if (bucketSelect) { if (bucketSelect) {
bucketSelect.addEventListener('change', async (event) => { bucketSelect.addEventListener('change', async (event) => {
selectedBucket = event.target.value; selectedBucket = event.target.value;

View File

@@ -195,6 +195,16 @@ const createValkeySession = async (res, credentials) => {
return sessionId; return sessionId;
}; };
const destroyValkeySession = async (req, res) => {
const cookies = parseCookieHeader(req.headers.cookie || '');
const sessionId = cookies[SESSION_COOKIE_NAME];
if (sessionId && valkeyEnabled && valkeyClient) {
await valkeyClient.del(getSessionStorageKey(sessionId));
console.log(`[auth] session cleared sessionId=${sessionId}`);
}
clearSessionCookie(res);
};
const loadValkeySession = async (req) => { const loadValkeySession = async (req) => {
if (!valkeyEnabled || !valkeyClient) { if (!valkeyEnabled || !valkeyClient) {
return null; return null;
@@ -602,7 +612,7 @@ app.get('/api/buckets', async (req, res) => {
await createValkeySession(res, auth); await createValkeySession(res, auth);
} }
console.log(`[s3] list buckets complete count=${buckets.length}`); console.log(`[s3] list buckets complete count=${buckets.length}`);
res.json({ buckets }); res.json({ buckets, username: auth.username || '' });
} catch (error) { } catch (error) {
if (auth?.source === 'session') { if (auth?.source === 'session') {
clearSessionCookie(res); clearSessionCookie(res);
@@ -612,6 +622,16 @@ app.get('/api/buckets', async (req, res) => {
} }
}); });
app.post('/api/logout', async (req, res) => {
try {
await destroyValkeySession(req, res);
res.json({ ok: true });
} catch (error) {
console.error('[auth] logout failed:', error);
res.status(500).json({ error: 'Failed to logout', detail: error.message });
}
});
// Endpoint to list videos in the bucket // Endpoint to list videos in the bucket
app.get('/api/videos', async (req, res) => { app.get('/api/videos', async (req, res) => {
try { try {