mirror of
https://github.com/LibreSpark/LibreTV.git
synced 2026-05-06 22:02:33 +08:00
feat: 新增支持设置按钮密码访问 (#594)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: 旋律已经死了。 <89735151+JohnsonRan@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# 基本配置
|
||||
PORT=8080
|
||||
PASSWORD=
|
||||
ADMINPASSWORD=
|
||||
DEBUG=false
|
||||
|
||||
# CORS 配置
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ coverage
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
13
README.md
13
README.md
@@ -75,7 +75,8 @@ Pull Bot 会反复触发无效的 PR 和垃圾邮件,严重干扰项目维护
|
||||
- 构建命令:留空(无需构建)
|
||||
- 输出目录:留空(默认为根目录)
|
||||
5. **⚠️ 重要:在"设置" > "环境变量"中添加 `PASSWORD` 变量**
|
||||
6. 点击"保存并部署"
|
||||
6. **可选:在"Settings" > "Environment Variables"中添加 `ADMINPASSWORD` 变量**
|
||||
7. 点击"保存并部署"
|
||||
|
||||
### Vercel
|
||||
|
||||
@@ -83,8 +84,9 @@ Pull Bot 会反复触发无效的 PR 和垃圾邮件,严重干扰项目维护
|
||||
2. 登录 [Vercel](https://vercel.com/),点击"New Project"
|
||||
3. 导入您的仓库,使用默认设置
|
||||
4. **⚠️ 重要:在"Settings" > "Environment Variables"中添加 `PASSWORD` 变量**
|
||||
5. 点击"Deploy"
|
||||
6. 可选:在"Settings" > "Environment Variables"中配置密码保护
|
||||
5. **可选:在"Settings" > "Environment Variables"中添加 `ADMINPASSWORD` 变量**
|
||||
6. 点击"Deploy"
|
||||
7. 可选:在"Settings" > "Environment Variables"中配置密码保护和设置按钮密码保护
|
||||
|
||||
### Docker
|
||||
```
|
||||
@@ -93,6 +95,7 @@ docker run -d \
|
||||
--restart unless-stopped \
|
||||
-p 8899:8080 \
|
||||
-e PASSWORD=your_password \
|
||||
-e ADMINPASSWORD=your_adminpassword \
|
||||
bestzwei/libretv:latest
|
||||
```
|
||||
|
||||
@@ -109,6 +112,7 @@ services:
|
||||
- "8899:8080" # 将内部 8080 端口映射到主机的 8899 端口
|
||||
environment:
|
||||
- PASSWORD=${PASSWORD:-your_password} # 可将 your_password 修改为你想要的密码,默认为 your_password
|
||||
- ADMINPASSWORD=${PASSWORD:-your_adminpassword} # 可将 your_adminpassword 修改为你想要的密码,默认为 your_adminpassword
|
||||
restart: unless-stopped
|
||||
```
|
||||
启动 LibreTV:
|
||||
@@ -146,6 +150,9 @@ npm run dev
|
||||
**环境变量名**: `PASSWORD`
|
||||
**值**: 您想设置的密码
|
||||
|
||||
**环境变量名**: `ADMINPASSWORD`
|
||||
**值**: 您想设置的密码
|
||||
|
||||
各平台设置方法:
|
||||
|
||||
- **Cloudflare Pages**: Dashboard > 您的项目 > 设置 > 环境变量
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { sha256 } from '../js/sha256.js'; // 需新建或引入SHA-256实现
|
||||
import { sha256 } from '../js/sha256.js';
|
||||
|
||||
// Cloudflare Pages Middleware to inject environment variables
|
||||
export async function onRequest(context) {
|
||||
const { request, env, next } = context;
|
||||
|
||||
// Proceed to the next middleware or route handler
|
||||
const response = await next();
|
||||
|
||||
// Check if the response is HTML
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
|
||||
if (contentType.includes("text/html")) {
|
||||
// Get the original HTML content
|
||||
let html = await response.text();
|
||||
|
||||
// Replace the placeholder with actual environment variable value
|
||||
// If PASSWORD is not set, replace with empty string
|
||||
// 处理普通密码
|
||||
const password = env.PASSWORD || "";
|
||||
let passwordHash = "";
|
||||
if (password) {
|
||||
passwordHash = await sha256(password);
|
||||
}
|
||||
html = html.replace('window.__ENV__.PASSWORD = "{{PASSWORD}}";',
|
||||
`window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash`);
|
||||
`window.__ENV__.PASSWORD = "${passwordHash}";`);
|
||||
|
||||
// 处理管理员密码 - 确保这部分代码被执行
|
||||
const adminPassword = env.ADMINPASSWORD || "";
|
||||
let adminPasswordHash = "";
|
||||
if (adminPassword) {
|
||||
adminPasswordHash = await sha256(adminPassword);
|
||||
}
|
||||
html = html.replace('window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}";',
|
||||
`window.__ENV__.ADMINPASSWORD = "${adminPasswordHash}";`);
|
||||
|
||||
// Create a new response with the modified HTML
|
||||
return new Response(html, {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
@@ -32,6 +33,5 @@ export async function onRequest(context) {
|
||||
});
|
||||
}
|
||||
|
||||
// Return the original response for non-HTML content
|
||||
return response;
|
||||
}
|
||||
@@ -428,6 +428,7 @@
|
||||
// 注入服务器端环境变量 (将由服务器端替换)
|
||||
// PASSWORD 变量将在这里被服务器端注入
|
||||
window.__ENV__.PASSWORD = "{{PASSWORD}}";
|
||||
window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}";
|
||||
</script>
|
||||
|
||||
<!-- 版本检测脚本 -->
|
||||
|
||||
316
js/app.js
316
js/app.js
@@ -12,51 +12,51 @@ let currentVideoTitle = '';
|
||||
let episodesReversed = false;
|
||||
|
||||
// 页面初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 初始化API复选框
|
||||
initAPICheckboxes();
|
||||
|
||||
|
||||
// 初始化自定义API列表
|
||||
renderCustomAPIsList();
|
||||
|
||||
|
||||
// 初始化显示选中的API数量
|
||||
updateSelectedApiCount();
|
||||
|
||||
|
||||
// 渲染搜索历史
|
||||
renderSearchHistory();
|
||||
|
||||
|
||||
// 设置默认API选择(如果是第一次加载)
|
||||
if (!localStorage.getItem('hasInitializedDefaults')) {
|
||||
// 默认选中资源
|
||||
selectedAPIs = ["tyyszy","bfzy","dyttzy", "ruyi"];
|
||||
selectedAPIs = ["tyyszy", "bfzy", "dyttzy", "ruyi"];
|
||||
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
||||
|
||||
|
||||
// 默认选中过滤开关
|
||||
localStorage.setItem('yellowFilterEnabled', 'true');
|
||||
localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, 'true');
|
||||
|
||||
|
||||
// 默认启用豆瓣功能
|
||||
localStorage.setItem('doubanEnabled', 'true');
|
||||
|
||||
// 标记已初始化默认值
|
||||
localStorage.setItem('hasInitializedDefaults', 'true');
|
||||
}
|
||||
|
||||
|
||||
// 设置黄色内容过滤器开关初始状态
|
||||
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
|
||||
if (yellowFilterToggle) {
|
||||
yellowFilterToggle.checked = localStorage.getItem('yellowFilterEnabled') === 'true';
|
||||
}
|
||||
|
||||
|
||||
// 设置广告过滤开关初始状态
|
||||
const adFilterToggle = document.getElementById('adFilterToggle');
|
||||
if (adFilterToggle) {
|
||||
adFilterToggle.checked = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true
|
||||
}
|
||||
|
||||
|
||||
// 设置事件监听器
|
||||
setupEventListeners();
|
||||
|
||||
|
||||
// 初始检查成人API选中状态
|
||||
setTimeout(checkAdultAPIsSelected, 100);
|
||||
});
|
||||
@@ -74,14 +74,14 @@ function initAPICheckboxes() {
|
||||
normalTitle.className = 'api-group-title';
|
||||
normalTitle.textContent = '普通资源';
|
||||
normaldiv.appendChild(normalTitle);
|
||||
|
||||
|
||||
// 创建普通API源的复选框
|
||||
Object.keys(API_SITES).forEach(apiKey => {
|
||||
const api = API_SITES[apiKey];
|
||||
if (api.adult) return; // 跳过成人内容API,稍后添加
|
||||
|
||||
|
||||
const checked = selectedAPIs.includes(apiKey);
|
||||
|
||||
|
||||
const checkbox = document.createElement('div');
|
||||
checkbox.className = 'flex items-center';
|
||||
checkbox.innerHTML = `
|
||||
@@ -92,9 +92,9 @@ function initAPICheckboxes() {
|
||||
<label for="api_${apiKey}" class="ml-1 text-xs text-gray-400 truncate">${api.name}</label>
|
||||
`;
|
||||
normaldiv.appendChild(checkbox);
|
||||
|
||||
|
||||
// 添加事件监听器
|
||||
checkbox.querySelector('input').addEventListener('change', function() {
|
||||
checkbox.querySelector('input').addEventListener('change', function () {
|
||||
updateSelectedAPIs();
|
||||
checkAdultAPIsSelected();
|
||||
});
|
||||
@@ -126,14 +126,14 @@ function addAdultAPI() {
|
||||
</svg>
|
||||
</span>`;
|
||||
adultdiv.appendChild(adultTitle);
|
||||
|
||||
|
||||
// 创建成人API源的复选框
|
||||
Object.keys(API_SITES).forEach(apiKey => {
|
||||
const api = API_SITES[apiKey];
|
||||
if (!api.adult) return; // 仅添加成人内容API
|
||||
|
||||
|
||||
const checked = selectedAPIs.includes(apiKey);
|
||||
|
||||
|
||||
const checkbox = document.createElement('div');
|
||||
checkbox.className = 'flex items-center';
|
||||
checkbox.innerHTML = `
|
||||
@@ -144,9 +144,9 @@ function addAdultAPI() {
|
||||
<label for="api_${apiKey}" class="ml-1 text-xs text-pink-400 truncate">${api.name}</label>
|
||||
`;
|
||||
adultdiv.appendChild(checkbox);
|
||||
|
||||
|
||||
// 添加事件监听器
|
||||
checkbox.querySelector('input').addEventListener('change', function() {
|
||||
checkbox.querySelector('input').addEventListener('change', function () {
|
||||
updateSelectedAPIs();
|
||||
checkAdultAPIsSelected();
|
||||
});
|
||||
@@ -159,30 +159,30 @@ function addAdultAPI() {
|
||||
function checkAdultAPIsSelected() {
|
||||
// 查找所有内置成人API复选框
|
||||
const adultBuiltinCheckboxes = document.querySelectorAll('#apiCheckboxes .api-adult:checked');
|
||||
|
||||
|
||||
// 查找所有自定义成人API复选框
|
||||
const customApiCheckboxes = document.querySelectorAll('#customApisList .api-adult:checked');
|
||||
|
||||
|
||||
const hasAdultSelected = adultBuiltinCheckboxes.length > 0 || customApiCheckboxes.length > 0;
|
||||
|
||||
|
||||
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
|
||||
const yellowFilterContainer = yellowFilterToggle.closest('div').parentNode;
|
||||
const filterDescription = yellowFilterContainer.querySelector('p.filter-description');
|
||||
|
||||
|
||||
// 如果选择了成人API,禁用黄色内容过滤器
|
||||
if (hasAdultSelected) {
|
||||
yellowFilterToggle.checked = false;
|
||||
yellowFilterToggle.disabled = true;
|
||||
localStorage.setItem('yellowFilterEnabled', 'false');
|
||||
|
||||
|
||||
// 添加禁用样式
|
||||
yellowFilterContainer.classList.add('filter-disabled');
|
||||
|
||||
|
||||
// 修改描述文字
|
||||
if (filterDescription) {
|
||||
filterDescription.innerHTML = '<strong class="text-pink-300">选中黄色资源站时无法启用此过滤</strong>';
|
||||
}
|
||||
|
||||
|
||||
// 移除提示信息(如果存在)
|
||||
const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip');
|
||||
if (existingTooltip) {
|
||||
@@ -192,12 +192,12 @@ function checkAdultAPIsSelected() {
|
||||
// 启用黄色内容过滤器
|
||||
yellowFilterToggle.disabled = false;
|
||||
yellowFilterContainer.classList.remove('filter-disabled');
|
||||
|
||||
|
||||
// 恢复原来的描述文字
|
||||
if (filterDescription) {
|
||||
filterDescription.innerHTML = '过滤"伦理片"等黄色内容';
|
||||
}
|
||||
|
||||
|
||||
// 移除提示信息
|
||||
const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip');
|
||||
if (existingTooltip) {
|
||||
@@ -210,12 +210,12 @@ function checkAdultAPIsSelected() {
|
||||
function renderCustomAPIsList() {
|
||||
const container = document.getElementById('customApisList');
|
||||
if (!container) return;
|
||||
|
||||
|
||||
if (customAPIs.length === 0) {
|
||||
container.innerHTML = '<p class="text-xs text-gray-500 text-center my-2">未添加自定义API</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
container.innerHTML = '';
|
||||
customAPIs.forEach((api, index) => {
|
||||
const apiItem = document.createElement('div');
|
||||
@@ -244,7 +244,7 @@ function renderCustomAPIsList() {
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(apiItem);
|
||||
apiItem.querySelector('input').addEventListener('change', function() {
|
||||
apiItem.querySelector('input').addEventListener('change', function () {
|
||||
updateSelectedAPIs();
|
||||
checkAdultAPIsSelected();
|
||||
});
|
||||
@@ -313,10 +313,10 @@ function cancelEditCustomApi() {
|
||||
document.getElementById('customApiDetail').value = '';
|
||||
const isAdultInput = document.getElementById('customApiIsAdult');
|
||||
if (isAdultInput) isAdultInput.checked = false;
|
||||
|
||||
|
||||
// 隐藏表单
|
||||
document.getElementById('addCustomApiForm').classList.add('hidden');
|
||||
|
||||
|
||||
// 恢复添加按钮
|
||||
restoreAddCustomApiButtons();
|
||||
}
|
||||
@@ -335,20 +335,20 @@ function restoreAddCustomApiButtons() {
|
||||
function updateSelectedAPIs() {
|
||||
// 获取所有内置API复选框
|
||||
const builtInApiCheckboxes = document.querySelectorAll('#apiCheckboxes input:checked');
|
||||
|
||||
|
||||
// 获取选中的内置API
|
||||
const builtInApis = Array.from(builtInApiCheckboxes).map(input => input.dataset.api);
|
||||
|
||||
|
||||
// 获取选中的自定义API
|
||||
const customApiCheckboxes = document.querySelectorAll('#customApisList input:checked');
|
||||
const customApiIndices = Array.from(customApiCheckboxes).map(input => 'custom_' + input.dataset.customIndex);
|
||||
|
||||
|
||||
// 合并内置和自定义API
|
||||
selectedAPIs = [...builtInApis, ...customApiIndices];
|
||||
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
||||
|
||||
|
||||
// 更新显示选中的API数量
|
||||
updateSelectedApiCount();
|
||||
}
|
||||
@@ -364,7 +364,7 @@ function updateSelectedApiCount() {
|
||||
// 全选或取消全选API
|
||||
function selectAllAPIs(selectAll = true, excludeAdult = false) {
|
||||
const checkboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]');
|
||||
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (excludeAdult && checkbox.classList.contains('api-adult')) {
|
||||
checkbox.checked = false;
|
||||
@@ -372,7 +372,7 @@ function selectAllAPIs(selectAll = true, excludeAdult = false) {
|
||||
checkbox.checked = selectAll;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
updateSelectedAPIs();
|
||||
checkAdultAPIsSelected();
|
||||
}
|
||||
@@ -395,7 +395,7 @@ function cancelAddCustomApi() {
|
||||
document.getElementById('customApiDetail').value = '';
|
||||
const isAdultInput = document.getElementById('customApiIsAdult');
|
||||
if (isAdultInput) isAdultInput.checked = false;
|
||||
|
||||
|
||||
// 确保按钮是添加按钮
|
||||
restoreAddCustomApiButtons();
|
||||
}
|
||||
@@ -428,7 +428,7 @@ function addCustomApi() {
|
||||
const newApiIndex = customAPIs.length - 1;
|
||||
selectedAPIs.push('custom_' + newApiIndex);
|
||||
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
||||
|
||||
|
||||
// 重新渲染自定义API列表
|
||||
renderCustomAPIsList();
|
||||
updateSelectedApiCount();
|
||||
@@ -444,17 +444,17 @@ function addCustomApi() {
|
||||
// 移除自定义API
|
||||
function removeCustomApi(index) {
|
||||
if (index < 0 || index >= customAPIs.length) return;
|
||||
|
||||
|
||||
const apiName = customAPIs[index].name;
|
||||
|
||||
|
||||
// 从列表中移除API
|
||||
customAPIs.splice(index, 1);
|
||||
localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
|
||||
|
||||
|
||||
// 从选中列表中移除此API
|
||||
const customApiId = 'custom_' + index;
|
||||
selectedAPIs = selectedAPIs.filter(id => id !== customApiId);
|
||||
|
||||
|
||||
// 更新大于此索引的自定义API索引
|
||||
selectedAPIs = selectedAPIs.map(id => {
|
||||
if (id.startsWith('custom_')) {
|
||||
@@ -465,38 +465,66 @@ function removeCustomApi(index) {
|
||||
}
|
||||
return id;
|
||||
});
|
||||
|
||||
|
||||
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
||||
|
||||
|
||||
// 重新渲染自定义API列表
|
||||
renderCustomAPIsList();
|
||||
|
||||
|
||||
// 更新选中的API数量
|
||||
updateSelectedApiCount();
|
||||
|
||||
|
||||
// 重新检查成人API选中状态
|
||||
checkAdultAPIsSelected();
|
||||
|
||||
|
||||
showToast('已移除自定义API: ' + apiName, 'info');
|
||||
}
|
||||
|
||||
function toggleSettings(e) {
|
||||
const settingsPanel = document.getElementById('settingsPanel');
|
||||
if (!settingsPanel) return;
|
||||
|
||||
// 检查是否有管理员密码
|
||||
const hasAdminPassword = window.__ENV__?.ADMINPASSWORD &&
|
||||
window.__ENV__.ADMINPASSWORD.length === 64 &&
|
||||
!/^0+$/.test(window.__ENV__.ADMINPASSWORD);
|
||||
|
||||
if (settingsPanel.classList.contains('show')) {
|
||||
settingsPanel.classList.remove('show');
|
||||
} else {
|
||||
// 只有设置了管理员密码且未验证时才拦截
|
||||
if (hasAdminPassword && !isAdminVerified()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showAdminPasswordModal();
|
||||
return;
|
||||
}
|
||||
settingsPanel.classList.add('show');
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupEventListeners() {
|
||||
// 回车搜索
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
document.getElementById('searchInput').addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
// 点击外部关闭设置面板和历史记录面板
|
||||
document.addEventListener('click', function(e) {
|
||||
document.addEventListener('click', function (e) {
|
||||
// 关闭设置面板
|
||||
const settingsPanel = document.querySelector('#settingsPanel.show');
|
||||
const settingsButton = document.querySelector('#settingsPanel .close-btn');
|
||||
|
||||
if (settingsPanel && settingsButton &&
|
||||
!settingsPanel.contains(e.target) &&
|
||||
|
||||
if (settingsPanel && settingsButton &&
|
||||
!settingsPanel.contains(e.target) &&
|
||||
!settingsButton.contains(e.target)) {
|
||||
settingsPanel.classList.remove('show');
|
||||
}
|
||||
@@ -504,18 +532,18 @@ function setupEventListeners() {
|
||||
// 关闭历史记录面板
|
||||
const historyPanel = document.querySelector('#historyPanel.show');
|
||||
const historyButton = document.querySelector('#historyPanel .close-btn');
|
||||
|
||||
if (historyPanel && historyButton &&
|
||||
!historyPanel.contains(e.target) &&
|
||||
|
||||
if (historyPanel && historyButton &&
|
||||
!historyPanel.contains(e.target) &&
|
||||
!historyButton.contains(e.target)) {
|
||||
historyPanel.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 黄色内容过滤开关事件绑定
|
||||
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
|
||||
if (yellowFilterToggle) {
|
||||
yellowFilterToggle.addEventListener('change', function(e) {
|
||||
yellowFilterToggle.addEventListener('change', function (e) {
|
||||
localStorage.setItem('yellowFilterEnabled', e.target.checked);
|
||||
|
||||
// 控制黄色内容接口的显示状态
|
||||
@@ -532,11 +560,11 @@ function setupEventListeners() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 广告过滤开关事件绑定
|
||||
const adFilterToggle = document.getElementById('adFilterToggle');
|
||||
if (adFilterToggle) {
|
||||
adFilterToggle.addEventListener('change', function(e) {
|
||||
adFilterToggle.addEventListener('change', function (e) {
|
||||
localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, e.target.checked);
|
||||
});
|
||||
}
|
||||
@@ -547,28 +575,28 @@ function resetSearchArea() {
|
||||
// 清理搜索结果
|
||||
document.getElementById('results').innerHTML = '';
|
||||
document.getElementById('searchInput').value = '';
|
||||
|
||||
|
||||
// 恢复搜索区域的样式
|
||||
document.getElementById('searchArea').classList.add('flex-1');
|
||||
document.getElementById('searchArea').classList.remove('mb-8');
|
||||
document.getElementById('resultsArea').classList.add('hidden');
|
||||
|
||||
|
||||
// 确保页脚正确显示,移除相对定位
|
||||
const footer = document.querySelector('.footer');
|
||||
if (footer) {
|
||||
footer.style.position = '';
|
||||
}
|
||||
|
||||
|
||||
// 如果有豆瓣功能,检查是否需要显示豆瓣推荐区域
|
||||
if (typeof updateDoubanVisibility === 'function') {
|
||||
updateDoubanVisibility();
|
||||
}
|
||||
|
||||
|
||||
// 重置URL为主页
|
||||
try {
|
||||
window.history.pushState(
|
||||
{},
|
||||
`LibreTV - 免费在线视频搜索与观看平台`,
|
||||
{},
|
||||
`LibreTV - 免费在线视频搜索与观看平台`,
|
||||
`/`
|
||||
);
|
||||
// 更新页面标题
|
||||
@@ -597,35 +625,35 @@ async function search() {
|
||||
}
|
||||
}
|
||||
const query = document.getElementById('searchInput').value.trim();
|
||||
|
||||
|
||||
if (!query) {
|
||||
showToast('请输入搜索内容', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (selectedAPIs.length === 0) {
|
||||
showToast('请至少选择一个API源', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
showLoading();
|
||||
|
||||
|
||||
try {
|
||||
// 保存搜索历史
|
||||
saveSearchHistory(query);
|
||||
|
||||
|
||||
// 从所有选中的API源搜索
|
||||
let allResults = [];
|
||||
const searchPromises = selectedAPIs.map(async (apiId) => {
|
||||
try {
|
||||
let apiUrl, apiName, apiBaseUrl;
|
||||
|
||||
|
||||
// 处理自定义API
|
||||
if (apiId.startsWith('custom_')) {
|
||||
const customIndex = apiId.replace('custom_', '');
|
||||
const customApi = getCustomApiInfo(customIndex);
|
||||
if (!customApi) return [];
|
||||
|
||||
|
||||
apiBaseUrl = customApi.url;
|
||||
apiUrl = apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query);
|
||||
apiName = customApi.name;
|
||||
@@ -636,28 +664,28 @@ async function search() {
|
||||
apiUrl = apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query);
|
||||
apiName = API_SITES[apiId].name;
|
||||
}
|
||||
|
||||
|
||||
// 添加超时处理
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
|
||||
const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), {
|
||||
headers: API_CONFIG.search.headers,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// 处理第一页结果
|
||||
const results = data.list.map(item => ({
|
||||
...item,
|
||||
@@ -665,41 +693,41 @@ async function search() {
|
||||
source_code: apiId,
|
||||
api_url: apiId.startsWith('custom_') ? getCustomApiInfo(apiId.replace('custom_', ''))?.url : undefined
|
||||
}));
|
||||
|
||||
|
||||
// 获取总页数
|
||||
const pageCount = data.pagecount || 1;
|
||||
// 确定需要获取的额外页数 (最多获取maxPages页)
|
||||
const pagesToFetch = Math.min(pageCount - 1, API_CONFIG.search.maxPages - 1);
|
||||
|
||||
|
||||
// 如果有额外页数,获取更多页的结果
|
||||
if (pagesToFetch > 0) {
|
||||
const additionalPagePromises = [];
|
||||
|
||||
|
||||
for (let page = 2; page <= pagesToFetch + 1; page++) {
|
||||
// 构建分页URL
|
||||
const pageUrl = apiBaseUrl + API_CONFIG.search.pagePath
|
||||
.replace('{query}', encodeURIComponent(query))
|
||||
.replace('{page}', page);
|
||||
|
||||
|
||||
// 创建获取额外页的Promise
|
||||
const pagePromise = (async () => {
|
||||
try {
|
||||
const pageController = new AbortController();
|
||||
const pageTimeoutId = setTimeout(() => pageController.abort(), 8000);
|
||||
|
||||
|
||||
const pageResponse = await fetch(PROXY_URL + encodeURIComponent(pageUrl), {
|
||||
headers: API_CONFIG.search.headers,
|
||||
signal: pageController.signal
|
||||
});
|
||||
|
||||
|
||||
clearTimeout(pageTimeoutId);
|
||||
|
||||
|
||||
if (!pageResponse.ok) return [];
|
||||
|
||||
|
||||
const pageData = await pageResponse.json();
|
||||
|
||||
|
||||
if (!pageData || !pageData.list || !Array.isArray(pageData.list)) return [];
|
||||
|
||||
|
||||
// 处理当前页结果
|
||||
return pageData.list.map(item => ({
|
||||
...item,
|
||||
@@ -712,13 +740,13 @@ async function search() {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
additionalPagePromises.push(pagePromise);
|
||||
}
|
||||
|
||||
|
||||
// 等待所有额外页的结果
|
||||
const additionalResults = await Promise.all(additionalPagePromises);
|
||||
|
||||
|
||||
// 合并所有页的结果
|
||||
additionalResults.forEach(pageResults => {
|
||||
if (pageResults.length > 0) {
|
||||
@@ -726,43 +754,43 @@ async function search() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.warn(`API ${apiId} 搜索失败:`, error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 等待所有搜索请求完成
|
||||
const resultsArray = await Promise.all(searchPromises);
|
||||
|
||||
|
||||
// 合并所有结果
|
||||
resultsArray.forEach(results => {
|
||||
if (Array.isArray(results) && results.length > 0) {
|
||||
allResults = allResults.concat(results);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 更新搜索结果计数
|
||||
const searchResultsCount = document.getElementById('searchResultsCount');
|
||||
if (searchResultsCount) {
|
||||
searchResultsCount.textContent = allResults.length;
|
||||
}
|
||||
|
||||
|
||||
// 显示结果区域,调整搜索区域
|
||||
document.getElementById('searchArea').classList.remove('flex-1');
|
||||
document.getElementById('searchArea').classList.add('mb-8');
|
||||
document.getElementById('resultsArea').classList.remove('hidden');
|
||||
|
||||
|
||||
// 隐藏豆瓣推荐区域(如果存在)
|
||||
const doubanArea = document.getElementById('doubanArea');
|
||||
if (doubanArea) {
|
||||
doubanArea.classList.add('hidden');
|
||||
}
|
||||
|
||||
|
||||
const resultsDiv = document.getElementById('results');
|
||||
|
||||
|
||||
// 如果没有结果
|
||||
if (!allResults || allResults.length === 0) {
|
||||
resultsDiv.innerHTML = `
|
||||
@@ -785,8 +813,8 @@ async function search() {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
// 使用HTML5 History API更新URL,不刷新页面
|
||||
window.history.pushState(
|
||||
{ search: query },
|
||||
`搜索: ${query} - LibreTV`,
|
||||
{ search: query },
|
||||
`搜索: ${query} - LibreTV`,
|
||||
`/s=${encodedQuery}`
|
||||
);
|
||||
// 更新页面标题
|
||||
@@ -799,7 +827,7 @@ async function search() {
|
||||
// 处理搜索结果过滤:如果启用了黄色内容过滤,则过滤掉分类含有敏感内容的项目
|
||||
const yellowFilterEnabled = localStorage.getItem('yellowFilterEnabled') === 'true';
|
||||
if (yellowFilterEnabled) {
|
||||
const banned = ['伦理片','福利','里番动漫','门事件','萝莉少女','制服诱惑','国产传媒','cosplay','黑丝诱惑','无码','日本无码','有码','日本有码','SWAG','网红主播', '色情片','同性片','福利视频','福利片'];
|
||||
const banned = ['伦理片', '福利', '里番动漫', '门事件', '萝莉少女', '制服诱惑', '国产传媒', 'cosplay', '黑丝诱惑', '无码', '日本无码', '有码', '日本有码', 'SWAG', '网红主播', '色情片', '同性片', '福利视频', '福利片'];
|
||||
allResults = allResults.filter(item => {
|
||||
const typeName = item.type_name || '';
|
||||
return !banned.some(keyword => typeName.includes(keyword));
|
||||
@@ -813,17 +841,17 @@ async function search() {
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
const sourceInfo = item.source_name ?
|
||||
const sourceInfo = item.source_name ?
|
||||
`<span class="bg-[#222] text-xs px-1.5 py-0.5 rounded-full">${item.source_name}</span>` : '';
|
||||
const sourceCode = item.source_code || '';
|
||||
|
||||
|
||||
// 添加API URL属性,用于详情获取
|
||||
const apiUrlAttr = item.api_url ?
|
||||
const apiUrlAttr = item.api_url ?
|
||||
`data-api-url="${item.api_url.replace(/"/g, '"')}"` : '';
|
||||
|
||||
|
||||
// 修改为水平卡片布局,图片在左侧,文本在右侧,并优化样式
|
||||
const hasCover = item.vod_pic && item.vod_pic.startsWith('http');
|
||||
|
||||
|
||||
return `
|
||||
<div class="card-hover bg-[#111] rounded-lg overflow-hidden cursor-pointer transition-all hover:scale-[1.02] h-full shadow-sm hover:shadow-md"
|
||||
onclick="showDetails('${safeId}','${safeName}','${sourceCode}')" ${apiUrlAttr}>
|
||||
@@ -842,12 +870,12 @@ async function search() {
|
||||
<h3 class="font-semibold mb-2 break-words line-clamp-2 ${hasCover ? '' : 'text-center'}" title="${safeName}">${safeName}</h3>
|
||||
|
||||
<div class="flex flex-wrap ${hasCover ? '' : 'justify-center'} gap-1 mb-2">
|
||||
${(item.type_name || '').toString().replace(/</g, '<') ?
|
||||
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-blue-500 text-blue-300">
|
||||
${(item.type_name || '').toString().replace(/</g, '<') ?
|
||||
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-blue-500 text-blue-300">
|
||||
${(item.type_name || '').toString().replace(/</g, '<')}
|
||||
</span>` : ''}
|
||||
${(item.vod_year || '') ?
|
||||
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-purple-500 text-purple-300">
|
||||
${(item.vod_year || '') ?
|
||||
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-purple-500 text-purple-300">
|
||||
${item.vod_year}
|
||||
</span>` : ''}
|
||||
</div>
|
||||
@@ -874,7 +902,7 @@ async function search() {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
|
||||
resultsDiv.innerHTML = safeResults;
|
||||
} catch (error) {
|
||||
console.error('搜索错误:', error);
|
||||
@@ -914,12 +942,12 @@ function hookInput() {
|
||||
|
||||
// 重写 value 属性的 getter 和 setter
|
||||
Object.defineProperty(input, 'value', {
|
||||
get: function() {
|
||||
get: function () {
|
||||
// 确保读取时返回字符串(即使原始值为 undefined/null)
|
||||
const originalValue = descriptor.get.call(this);
|
||||
return originalValue != null ? String(originalValue) : '';
|
||||
},
|
||||
set: function(value) {
|
||||
set: function (value) {
|
||||
// 显式将值转换为字符串后写入
|
||||
const strValue = String(value);
|
||||
descriptor.set.call(this, strValue);
|
||||
@@ -945,12 +973,12 @@ async function showDetails(id, vod_name, sourceCode) {
|
||||
showToast('视频ID无效', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
// 构建API参数
|
||||
let apiParams = '';
|
||||
|
||||
|
||||
// 处理自定义API源
|
||||
if (sourceCode.startsWith('custom_')) {
|
||||
const customIndex = sourceCode.replace('custom_', '');
|
||||
@@ -970,26 +998,26 @@ async function showDetails(id, vod_name, sourceCode) {
|
||||
// 内置API
|
||||
apiParams = '&source=' + sourceCode;
|
||||
}
|
||||
|
||||
|
||||
// Add a timestamp to prevent caching
|
||||
const timestamp = new Date().getTime();
|
||||
const cacheBuster = `&_t=${timestamp}`;
|
||||
const response = await fetch(`/api/detail?id=${encodeURIComponent(id)}${apiParams}${cacheBuster}`);
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
const modal = document.getElementById('modal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalContent = document.getElementById('modalContent');
|
||||
|
||||
|
||||
// 显示来源信息
|
||||
const sourceName = data.videoInfo && data.videoInfo.source_name ?
|
||||
const sourceName = data.videoInfo && data.videoInfo.source_name ?
|
||||
` <span class="text-sm font-normal text-gray-400">(${data.videoInfo.source_name})</span>` : '';
|
||||
|
||||
|
||||
// 不对标题进行截断处理,允许完整显示
|
||||
modalTitle.innerHTML = `<span class="break-words">${vod_name || '未知视频'}</span>${sourceName}`;
|
||||
currentVideoTitle = vod_name || '未知视频';
|
||||
|
||||
|
||||
if (data.episodes && data.episodes.length > 0) {
|
||||
// 构建详情信息HTML
|
||||
let detailInfoHtml = '';
|
||||
@@ -1021,10 +1049,10 @@ async function showDetails(id, vod_name, sourceCode) {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
currentEpisodes = data.episodes;
|
||||
currentEpisodeIndex = 0;
|
||||
|
||||
|
||||
modalContent.innerHTML = `
|
||||
${detailInfoHtml}
|
||||
<div class="flex flex-wrap items-center justify-between mb-4 gap-2">
|
||||
@@ -1054,7 +1082,7 @@ async function showDetails(id, vod_name, sourceCode) {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('获取详情错误:', error);
|
||||
@@ -1073,18 +1101,18 @@ function playVideo(url, vod_name, sourceCode, episodeIndex = 0, vodId = '') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取当前路径作为返回页面
|
||||
let currentPath = window.location.href;
|
||||
|
||||
|
||||
// 构建播放页面URL,使用watch.html作为中间跳转页
|
||||
let watchUrl = `watch.html?id=${vodId || ''}&source=${sourceCode || ''}&url=${encodeURIComponent(url)}&index=${episodeIndex}&title=${encodeURIComponent(vod_name || '')}`;
|
||||
|
||||
|
||||
// 添加返回URL参数
|
||||
if (currentPath.includes('index.html') || currentPath.endsWith('/')) {
|
||||
watchUrl += `&back=${encodeURIComponent(currentPath)}`;
|
||||
}
|
||||
|
||||
|
||||
// 保存当前状态到localStorage
|
||||
try {
|
||||
localStorage.setItem('currentVideoTitle', vod_name || '未知视频');
|
||||
@@ -1097,7 +1125,7 @@ function playVideo(url, vod_name, sourceCode, episodeIndex = 0, vodId = '') {
|
||||
} catch (e) {
|
||||
console.error('保存播放状态失败:', e);
|
||||
}
|
||||
|
||||
|
||||
// 在当前标签页中打开播放页面
|
||||
window.location.href = watchUrl;
|
||||
}
|
||||
@@ -1203,7 +1231,7 @@ function toggleEpisodeOrder(sourceCode, vodId) {
|
||||
if (episodesGrid) {
|
||||
episodesGrid.innerHTML = renderEpisodes(currentVideoTitle, sourceCode, vodId);
|
||||
}
|
||||
|
||||
|
||||
// 更新按钮文本和箭头方向
|
||||
const toggleBtn = document.querySelector(`button[onclick="toggleEpisodeOrder('${sourceCode}', '${vodId}')"]`);
|
||||
if (toggleBtn) {
|
||||
@@ -1277,7 +1305,7 @@ async function importConfigFromUrl() {
|
||||
}
|
||||
|
||||
showLoading('正在从URL导入配置...');
|
||||
|
||||
|
||||
try {
|
||||
// 获取配置文件 - 直接请求URL
|
||||
const response = await fetch(url, {
|
||||
@@ -1305,7 +1333,7 @@ async function importConfigFromUrl() {
|
||||
for (let item in config.data) {
|
||||
localStorage.setItem(item, config.data[item]);
|
||||
}
|
||||
|
||||
|
||||
showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
@@ -1335,7 +1363,7 @@ async function importConfig() {
|
||||
if (!(file.type === 'application/json' || file.name.endsWith('.json'))) throw '文件类型不正确';
|
||||
|
||||
// 检查文件大小
|
||||
if(file.size > 1024 * 1024 * 10) throw new Error('文件大小超过 10MB');
|
||||
if (file.size > 1024 * 1024 * 10) throw new Error('文件大小超过 10MB');
|
||||
|
||||
// 读取文件内容
|
||||
const content = await new Promise((resolve, reject) => {
|
||||
@@ -1357,7 +1385,7 @@ async function importConfig() {
|
||||
for (let item in config.data) {
|
||||
localStorage.setItem(item, config.data[item]);
|
||||
}
|
||||
|
||||
|
||||
showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
|
||||
@@ -8,6 +8,7 @@ const MAX_HISTORY_ITEMS = 5;
|
||||
const PASSWORD_CONFIG = {
|
||||
localStorageKey: 'passwordVerified', // 存储验证状态的键名
|
||||
verificationTTL: 90 * 24 * 60 * 60 * 1000, // 验证有效期(90天,约3个月)
|
||||
adminLocalStorageKey: 'adminPasswordVerified' // 新增的管理员验证状态的键名
|
||||
};
|
||||
|
||||
// 网站信息配置
|
||||
|
||||
185
js/password.js
185
js/password.js
@@ -7,63 +7,77 @@
|
||||
function isPasswordProtected() {
|
||||
// 检查页面上嵌入的环境变量
|
||||
const pwd = window.__ENV__ && window.__ENV__.PASSWORD;
|
||||
// 只有当密码 hash 存在且为64位(SHA-256十六进制长度)才认为启用密码保护
|
||||
return typeof pwd === 'string' && pwd.length === 64 && !/^0+$/.test(pwd);
|
||||
}
|
||||
const adminPwd = window.__ENV__ && window.__ENV__.ADMINPASSWORD;
|
||||
|
||||
/**
|
||||
* 检查用户是否已通过密码验证
|
||||
* 检查localStorage中的验证状态和时间戳是否有效,并确认密码哈希未更改
|
||||
*/
|
||||
function isPasswordVerified() {
|
||||
try {
|
||||
// 如果没有设置密码保护,则视为已验证
|
||||
if (!isPasswordProtected()) {
|
||||
return true;
|
||||
}
|
||||
// 检查普通密码或管理员密码是否有效
|
||||
const isPwdValid = typeof pwd === 'string' && pwd.length === 64 && !/^0+$/.test(pwd);
|
||||
const isAdminPwdValid = typeof adminPwd === 'string' && adminPwd.length === 64 && !/^0+$/.test(adminPwd);
|
||||
|
||||
const verificationData = JSON.parse(localStorage.getItem(PASSWORD_CONFIG.localStorageKey) || '{}');
|
||||
const { verified, timestamp, passwordHash } = verificationData;
|
||||
|
||||
// 获取当前环境中的密码哈希
|
||||
const currentHash = window.__ENV__ && window.__ENV__.PASSWORD;
|
||||
|
||||
// 验证是否已验证、未过期,且密码哈希未更改
|
||||
if (verified && timestamp && passwordHash === currentHash) {
|
||||
const now = Date.now();
|
||||
const expiry = timestamp + PASSWORD_CONFIG.verificationTTL;
|
||||
return now < expiry;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('验证密码状态时出错:', error);
|
||||
return false;
|
||||
}
|
||||
// 任意一个密码有效即认为启用了密码保护
|
||||
return isPwdValid || isAdminPwdValid;
|
||||
}
|
||||
|
||||
window.isPasswordProtected = isPasswordProtected;
|
||||
window.isPasswordVerified = isPasswordVerified;
|
||||
|
||||
/**
|
||||
* 验证用户输入的密码是否正确(异步,使用SHA-256哈希)
|
||||
*/
|
||||
async function verifyPassword(password) {
|
||||
const correctHash = window.__ENV__ && window.__ENV__.PASSWORD;
|
||||
if (!correctHash) return false;
|
||||
const inputHash = await sha256(password);
|
||||
const isValid = inputHash === correctHash;
|
||||
if (isValid) {
|
||||
const verificationData = {
|
||||
verified: true,
|
||||
timestamp: Date.now(),
|
||||
passwordHash: correctHash // 保存当前密码的哈希值
|
||||
};
|
||||
localStorage.setItem(PASSWORD_CONFIG.localStorageKey, JSON.stringify(verificationData));
|
||||
// 统一验证函数
|
||||
async function verifyPassword(password, passwordType = 'PASSWORD') {
|
||||
try {
|
||||
const correctHash = window.__ENV__?.[passwordType];
|
||||
if (!correctHash) return false;
|
||||
|
||||
const inputHash = await sha256(password);
|
||||
const isValid = inputHash === correctHash;
|
||||
|
||||
if (isValid) {
|
||||
const storageKey = passwordType === 'PASSWORD'
|
||||
? PASSWORD_CONFIG.localStorageKey
|
||||
: PASSWORD_CONFIG.adminLocalStorageKey;
|
||||
|
||||
localStorage.setItem(storageKey, JSON.stringify({
|
||||
verified: true,
|
||||
timestamp: Date.now(),
|
||||
passwordHash: correctHash
|
||||
}));
|
||||
}
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
console.error(`验证${passwordType}密码时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// 统一验证状态检查
|
||||
function isVerified(passwordType = 'PASSWORD') {
|
||||
try {
|
||||
if (!isPasswordProtected()) return true;
|
||||
|
||||
const storageKey = passwordType === 'PASSWORD'
|
||||
? PASSWORD_CONFIG.localStorageKey
|
||||
: PASSWORD_CONFIG.adminLocalStorageKey;
|
||||
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (!stored) return false;
|
||||
|
||||
const { timestamp, passwordHash } = JSON.parse(stored);
|
||||
const currentHash = window.__ENV__?.[passwordType];
|
||||
|
||||
return timestamp && passwordHash === currentHash &&
|
||||
Date.now() - timestamp < PASSWORD_CONFIG.verificationTTL;
|
||||
} catch (error) {
|
||||
console.error(`检查${passwordType}验证状态时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新全局导出
|
||||
window.isPasswordProtected = isPasswordProtected;
|
||||
window.isPasswordVerified = () => isVerified('PASSWORD');
|
||||
window.isAdminVerified = () => isVerified('ADMINPASSWORD');
|
||||
window.verifyPassword = verifyPassword;
|
||||
|
||||
// SHA-256实现,可用Web Crypto API
|
||||
async function sha256(message) {
|
||||
if (window.crypto && crypto.subtle && crypto.subtle.digest) {
|
||||
@@ -89,7 +103,7 @@ function showPasswordModal() {
|
||||
document.getElementById('doubanArea').classList.add('hidden');
|
||||
|
||||
passwordModal.style.display = 'flex';
|
||||
|
||||
|
||||
// 确保输入框获取焦点
|
||||
setTimeout(() => {
|
||||
const passwordInput = document.getElementById('passwordInput');
|
||||
@@ -160,32 +174,71 @@ async function handlePasswordSubmit() {
|
||||
/**
|
||||
* 初始化密码验证系统(需适配异步事件)
|
||||
*/
|
||||
// 修改initPasswordProtection函数
|
||||
function initPasswordProtection() {
|
||||
if (!isPasswordProtected()) {
|
||||
return; // 如果未设置密码保护,则不进行任何操作
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果未验证密码,则显示密码验证弹窗
|
||||
if (!isPasswordVerified()) {
|
||||
// 检查是否有普通密码
|
||||
const hasNormalPassword = window.__ENV__?.PASSWORD &&
|
||||
window.__ENV__.PASSWORD.length === 64 &&
|
||||
!/^0+$/.test(window.__ENV__.PASSWORD);
|
||||
|
||||
// 只有当设置了普通密码且未验证时才显示密码框
|
||||
if (hasNormalPassword && !isPasswordVerified()) {
|
||||
showPasswordModal();
|
||||
|
||||
// 设置密码提交按钮事件监听
|
||||
const submitButton = document.getElementById('passwordSubmitBtn');
|
||||
if (submitButton) {
|
||||
submitButton.addEventListener('click', handlePasswordSubmit);
|
||||
}
|
||||
|
||||
// 设置密码输入框回车键监听
|
||||
const passwordInput = document.getElementById('passwordInput');
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
handlePasswordSubmit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 设置按钮事件监听
|
||||
const settingsBtn = document.querySelector('[onclick="toggleSettings(event)"]');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.addEventListener('click', function(e) {
|
||||
// 只有当设置了普通密码且未验证时才拦截点击
|
||||
if (hasNormalPassword && !isPasswordVerified()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showPasswordModal();
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 设置按钮密码框验证
|
||||
function showAdminPasswordModal() {
|
||||
const passwordModal = document.getElementById('passwordModal');
|
||||
if (!passwordModal) return;
|
||||
|
||||
// 清空密码输入框
|
||||
const passwordInput = document.getElementById('passwordInput');
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
|
||||
// 修改标题为管理员验证
|
||||
const title = passwordModal.querySelector('h2');
|
||||
if (title) title.textContent = '管理员验证';
|
||||
passwordModal.style.display = 'flex';
|
||||
|
||||
// 设置表单提交处理
|
||||
const form = document.getElementById('passwordForm');
|
||||
if (form) {
|
||||
form.onsubmit = async function (e) {
|
||||
e.preventDefault();
|
||||
const password = document.getElementById('passwordInput').value.trim();
|
||||
if (await verifyPassword(password, 'ADMINPASSWORD')) {
|
||||
passwordModal.style.display = 'none';
|
||||
document.getElementById('settingsPanel').classList.add('show');
|
||||
} else {
|
||||
showPasswordError();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 在页面加载完成后初始化密码保护
|
||||
document.addEventListener('DOMContentLoaded', initPasswordProtection);
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initPasswordProtection();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -30,12 +30,25 @@ export default async function middleware(request) {
|
||||
if (password) {
|
||||
passwordHash = await sha256(password);
|
||||
}
|
||||
const modifiedHtml = originalHtml.replace(
|
||||
'window.__ENV__.PASSWORD = "{{PASSWORD}}";',
|
||||
`window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash`
|
||||
);
|
||||
|
||||
const adminpassword = process.env.ADMINPASSWORD || '';
|
||||
let adminpasswordHash = '';
|
||||
if (adminpassword) {
|
||||
adminpasswordHash = await sha256(adminpassword); // 修复变量名
|
||||
}
|
||||
|
||||
// Create a new response with the modified HTML
|
||||
// 合并两次替换为一次操作
|
||||
let modifiedHtml = originalHtml
|
||||
.replace(
|
||||
'window.__ENV__.PASSWORD = "{{PASSWORD}}";',
|
||||
`window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash`
|
||||
)
|
||||
.replace(
|
||||
'window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}";',
|
||||
`window.__ENV__.ADMINPASSWORD = "${adminpasswordHash}"; // SHA-256 hash`
|
||||
);
|
||||
|
||||
// 修复Response构造
|
||||
return new Response(modifiedHtml, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
// 注入服务器端环境变量 (将由服务器端替换)
|
||||
// PASSWORD 变量将在这里被服务器端注入
|
||||
window.__ENV__.PASSWORD = "{{PASSWORD}}";
|
||||
window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}";
|
||||
|
||||
// 修复 home 跳转
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
13
server.mjs
13
server.mjs
@@ -15,6 +15,7 @@ const __dirname = path.dirname(__filename);
|
||||
const config = {
|
||||
port: process.env.PORT || 8080,
|
||||
password: process.env.PASSWORD || '',
|
||||
adminpassword: process.env.ADMINPASSWORD || '',
|
||||
corsOrigin: process.env.CORS_ORIGIN || '*',
|
||||
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
|
||||
maxRetries: parseInt(process.env.MAX_RETRIES || '2'),
|
||||
@@ -58,6 +59,11 @@ async function renderPage(filePath, password) {
|
||||
const sha256 = await sha256Hash(password);
|
||||
content = content.replace('{{PASSWORD}}', sha256);
|
||||
}
|
||||
// 添加ADMINPASSWORD注入
|
||||
if (config.adminpassword !== '') {
|
||||
const adminSha256 = await sha256Hash(config.adminpassword);
|
||||
content = content.replace('{{ADMINPASSWORD}}', adminSha256);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -196,10 +202,13 @@ app.use((req, res) => {
|
||||
app.listen(config.port, () => {
|
||||
console.log(`服务器运行在 http://localhost:${config.port}`);
|
||||
if (config.password !== '') {
|
||||
console.log('登录密码已设置');
|
||||
console.log('用户登录密码已设置');
|
||||
}
|
||||
if (config.adminpassword !== '') {
|
||||
console.log('管理员登录密码已设置');
|
||||
}
|
||||
if (config.debug) {
|
||||
console.log('调试模式已启用');
|
||||
console.log('配置:', { ...config, password: config.password ? '******' : '' });
|
||||
console.log('配置:', { ...config, password: config.password ? '******' : '', adminpassword: config.adminpassword? '******' : '' });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user