mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-28 01:13:15 +08:00
feat: add user api limit
This commit is contained in:
@@ -10,7 +10,7 @@ const nextConfig = {
|
|||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
|
|
||||||
experimental: {
|
experimental: {
|
||||||
instrumentationHook: true,
|
instrumentationHook: process.env.NODE_ENV === 'production',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Uncoment to add domain whitelist
|
// Uncoment to add domain whitelist
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GripVertical } from 'lucide-react';
|
import { GripVertical } from 'lucide-react';
|
||||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
||||||
@@ -145,6 +146,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
});
|
});
|
||||||
|
const [showConfigureApisModal, setShowConfigureApisModal] = useState(false);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<{
|
||||||
|
username: string;
|
||||||
|
role: 'user' | 'admin' | 'owner';
|
||||||
|
enabledApis?: string[];
|
||||||
|
} | null>(null);
|
||||||
|
const [selectedApis, setSelectedApis] = useState<string[]>([]);
|
||||||
|
|
||||||
// 当前登录用户名
|
// 当前登录用户名
|
||||||
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
||||||
@@ -209,6 +217,56 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
await handleUserAction('deleteUser', username);
|
await handleUserAction('deleteUser', username);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConfigureUserApis = (user: {
|
||||||
|
username: string;
|
||||||
|
role: 'user' | 'admin' | 'owner';
|
||||||
|
enabledApis?: string[];
|
||||||
|
}) => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setSelectedApis(user.enabledApis || []);
|
||||||
|
setShowConfigureApisModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取URL域名的辅助函数
|
||||||
|
const extractDomain = (url: string): string => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
return urlObj.hostname;
|
||||||
|
} catch {
|
||||||
|
// 如果URL格式不正确,返回原字符串
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveUserApis = async () => {
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
targetUsername: selectedUser.username,
|
||||||
|
action: 'updateUserApis',
|
||||||
|
enabledApis: selectedApis,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || `操作失败: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功后刷新配置
|
||||||
|
await refreshConfig();
|
||||||
|
setShowConfigureApisModal(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
setSelectedApis([]);
|
||||||
|
} catch (err) {
|
||||||
|
showError(err instanceof Error ? err.message : '操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 通用请求函数
|
// 通用请求函数
|
||||||
const handleUserAction = async (
|
const handleUserAction = async (
|
||||||
action:
|
action:
|
||||||
@@ -394,6 +452,12 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
>
|
>
|
||||||
状态
|
状态
|
||||||
</th>
|
</th>
|
||||||
|
<th
|
||||||
|
scope='col'
|
||||||
|
className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'
|
||||||
|
>
|
||||||
|
采集源权限
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
scope='col'
|
scope='col'
|
||||||
className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'
|
className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'
|
||||||
@@ -470,6 +534,27 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
{!user.banned ? '正常' : '已封禁'}
|
{!user.banned ? '正常' : '已封禁'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td className='px-6 py-4 whitespace-nowrap'>
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
<span className='text-sm text-gray-900 dark:text-gray-100'>
|
||||||
|
{user.enabledApis && user.enabledApis.length > 0
|
||||||
|
? `${user.enabledApis.length} 个源`
|
||||||
|
: '无限制'}
|
||||||
|
</span>
|
||||||
|
{/* 配置采集源权限按钮 */}
|
||||||
|
{(role === 'owner' ||
|
||||||
|
(role === 'admin' &&
|
||||||
|
(user.role === 'user' ||
|
||||||
|
user.username === currentUsername))) && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleConfigureUserApis(user)}
|
||||||
|
className='inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors'
|
||||||
|
>
|
||||||
|
配置
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
||||||
{/* 修改密码按钮 */}
|
{/* 修改密码按钮 */}
|
||||||
{canChangePassword && (
|
{canChangePassword && (
|
||||||
@@ -542,6 +627,131 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 配置用户采集源权限弹窗 */}
|
||||||
|
{showConfigureApisModal && selectedUser && createPortal(
|
||||||
|
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
|
||||||
|
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='flex items-center justify-between mb-6'>
|
||||||
|
<h3 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
配置用户采集源权限 - {selectedUser.username}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowConfigureApisModal(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
setSelectedApis([]);
|
||||||
|
}}
|
||||||
|
className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
|
||||||
|
>
|
||||||
|
<svg className='w-6 h-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||||
|
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M6 18L18 6M6 6l12 12' />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-6'>
|
||||||
|
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4'>
|
||||||
|
<div className='flex items-center space-x-2 mb-2'>
|
||||||
|
<svg className='w-5 h-5 text-blue-600 dark:text-blue-400' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||||
|
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' />
|
||||||
|
</svg>
|
||||||
|
<span className='text-sm font-medium text-blue-800 dark:text-blue-300'>
|
||||||
|
配置说明
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-sm text-blue-700 dark:text-blue-400 mt-1'>
|
||||||
|
提示:全不选为无限制,选中的采集源将限制用户只能访问这些源
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 采集源选择 - 多列布局 */}
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
|
||||||
|
选择可用的采集源:
|
||||||
|
</h4>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
|
||||||
|
{config?.SourceConfig?.map((source) => (
|
||||||
|
<label key={source.key} className='flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={selectedApis.includes(source.key)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedApis([...selectedApis, source.key]);
|
||||||
|
} else {
|
||||||
|
setSelectedApis(selectedApis.filter(api => api !== source.key));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700'
|
||||||
|
/>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate'>
|
||||||
|
{source.name}
|
||||||
|
</div>
|
||||||
|
{source.api && (
|
||||||
|
<div className='text-xs text-gray-500 dark:text-gray-400 truncate'>
|
||||||
|
{extractDomain(source.api)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 快速操作按钮 */}
|
||||||
|
<div className='flex flex-wrap items-center justify-between mb-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg'>
|
||||||
|
<div className='flex space-x-2'>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedApis([])}
|
||||||
|
className='px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors'
|
||||||
|
>
|
||||||
|
全不选(无限制)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const allApis = config?.SourceConfig?.filter(source => !source.disabled).map(s => s.key) || [];
|
||||||
|
setSelectedApis(allApis);
|
||||||
|
}}
|
||||||
|
className='px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors'
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-gray-600 dark:text-gray-400'>
|
||||||
|
已选择:<span className='font-medium text-blue-600 dark:text-blue-400'>
|
||||||
|
{selectedApis.length > 0 ? `${selectedApis.length} 个源` : '无限制'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className='flex justify-end space-x-3'>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowConfigureApisModal(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
setSelectedApis([]);
|
||||||
|
}}
|
||||||
|
className='px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveUserApis}
|
||||||
|
className='px-6 py-2.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
确认配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const ACTIONS = [
|
|||||||
'cancelAdmin',
|
'cancelAdmin',
|
||||||
'changePassword',
|
'changePassword',
|
||||||
'deleteUser',
|
'deleteUser',
|
||||||
|
'updateUserApis',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -60,6 +61,7 @@ export async function POST(request: NextRequest) {
|
|||||||
if (
|
if (
|
||||||
action !== 'changePassword' &&
|
action !== 'changePassword' &&
|
||||||
action !== 'deleteUser' &&
|
action !== 'deleteUser' &&
|
||||||
|
action !== 'updateUserApis' &&
|
||||||
username === targetUsername
|
username === targetUsername
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -273,6 +275,38 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'updateUserApis': {
|
||||||
|
if (!targetEntry) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '目标用户不存在' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { enabledApis } = body as { enabledApis?: string[] };
|
||||||
|
|
||||||
|
// 权限检查:站长可配置所有人的采集源,管理员可配置普通用户和自己的采集源
|
||||||
|
if (
|
||||||
|
isTargetAdmin &&
|
||||||
|
operatorRole !== 'owner' &&
|
||||||
|
username !== targetUsername
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '仅站长可配置其他管理员的采集源' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户的采集源权限
|
||||||
|
if (enabledApis && enabledApis.length > 0) {
|
||||||
|
targetEntry.enabledApis = enabledApis;
|
||||||
|
} else {
|
||||||
|
// 如果为空数组或未提供,则删除该字段,表示无限制
|
||||||
|
delete targetEntry.enabledApis;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ async function verifyDevice(): Promise<void> {
|
|||||||
if (!apiResp.success) {
|
if (!apiResp.success) {
|
||||||
console.error('❌ 设备验证失败');
|
console.error('❌ 设备验证失败');
|
||||||
console.error(`验证失败原因: ${apiResp.message}`);
|
console.error(`验证失败原因: ${apiResp.message}`);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置网络失败计数
|
// 重置网络失败计数
|
||||||
@@ -244,13 +244,13 @@ async function verifyDevice(): Promise<void> {
|
|||||||
|
|
||||||
if (networkFailureCount >= MAX_NETWORK_FAILURES) {
|
if (networkFailureCount >= MAX_NETWORK_FAILURES) {
|
||||||
console.error('❌ 网络验证失败次数超过限制,重置认证信息');
|
console.error('❌ 网络验证失败次数超过限制,重置认证信息');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非网络错误,直接退出
|
// 非网络错误,直接退出
|
||||||
console.error('❌ 设备验证失败');
|
console.error('❌ 设备验证失败');
|
||||||
console.error(`验证失败原因: ${errorMessage}`);
|
console.error(`验证失败原因: ${errorMessage}`);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,7 +298,7 @@ async function initializeDeviceAuth(): Promise<void> {
|
|||||||
});
|
});
|
||||||
} catch (keyError) {
|
} catch (keyError) {
|
||||||
console.error('❌ 公钥KeyObject创建失败:', keyError);
|
console.error('❌ 公钥KeyObject创建失败:', keyError);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
expectedFingerprint = fingerprint;
|
expectedFingerprint = fingerprint;
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ async function initializeDeviceAuth(): Promise<void> {
|
|||||||
console.log('🔑 设备认证信息初始化成功');
|
console.log('🔑 设备认证信息初始化成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 设备认证信息初始化失败:', error);
|
console.error('❌ 设备认证信息初始化失败:', error);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAvailableApiSites, getCacheTime } from '@/lib/config';
|
import { getAvailableApiSites, getCacheTime } from '@/lib/config';
|
||||||
import { getDetailFromApi } from '@/lib/downstream';
|
import { getDetailFromApi } from '@/lib/downstream';
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
const sourceCode = searchParams.get('source');
|
const sourceCode = searchParams.get('source');
|
||||||
@@ -19,7 +25,7 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiSites = await getAvailableApiSites();
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
const apiSite = apiSites.find((site) => site.key === sourceCode);
|
const apiSite = apiSites.find((site) => site.key === sourceCode);
|
||||||
|
|
||||||
if (!apiSite) {
|
if (!apiSite) {
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getCacheTime, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { yellowWords } from '@/lib/yellow';
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
// OrionTV 兼容接口
|
// OrionTV 兼容接口
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const query = searchParams.get('q');
|
const query = searchParams.get('q');
|
||||||
const resourceId = searchParams.get('resourceId');
|
const resourceId = searchParams.get('resourceId');
|
||||||
@@ -28,7 +34,7 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const apiSites = config.SourceConfig.filter((site) => !site.disabled);
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 根据 resourceId 查找对应的 API 站点
|
// 根据 resourceId 查找对应的 API 站点
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getCacheTime, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
import { yellowWords } from '@/lib/yellow';
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const query = searchParams.get('q');
|
const query = searchParams.get('q');
|
||||||
|
|
||||||
@@ -28,7 +34,7 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const apiSites = config.SourceConfig.filter((site) => !site.disabled);
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
|
|
||||||
// 添加超时控制和错误处理,避免慢接口拖累整体响应
|
// 添加超时控制和错误处理,避免慢接口拖累整体响应
|
||||||
const searchPromises = apiSites.map((site) =>
|
const searchPromises = apiSites.map((site) =>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成建议
|
// 生成建议
|
||||||
const suggestions = await generateSuggestions(query);
|
const suggestions = await generateSuggestions(query, authInfo.username);
|
||||||
|
|
||||||
// 从配置中获取缓存时间,如果没有配置则使用默认值300秒(5分钟)
|
// 从配置中获取缓存时间,如果没有配置则使用默认值300秒(5分钟)
|
||||||
const cacheTime = config.SiteConfig.SiteInterfaceCacheTime || 300;
|
const cacheTime = config.SiteConfig.SiteInterfaceCacheTime || 300;
|
||||||
@@ -57,7 +57,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateSuggestions(query: string): Promise<
|
async function generateSuggestions(query: string, username: string): Promise<
|
||||||
Array<{
|
Array<{
|
||||||
text: string;
|
text: string;
|
||||||
type: 'exact' | 'related' | 'suggestion';
|
type: 'exact' | 'related' | 'suggestion';
|
||||||
@@ -66,8 +66,7 @@ async function generateSuggestions(query: string): Promise<
|
|||||||
> {
|
> {
|
||||||
const queryLower = query.toLowerCase();
|
const queryLower = query.toLowerCase();
|
||||||
|
|
||||||
const config = await getConfig();
|
const apiSites = await getAvailableApiSites(username);
|
||||||
const apiSites = config.SourceConfig.filter((site: any) => !site.disabled);
|
|
||||||
let realKeywords: string[] = [];
|
let realKeywords: string[] = [];
|
||||||
|
|
||||||
if (apiSites.length > 0) {
|
if (apiSites.length > 0) {
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
import { yellowWords } from '@/lib/yellow';
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const query = searchParams.get('q');
|
const query = searchParams.get('q');
|
||||||
|
|
||||||
@@ -25,7 +31,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const apiSites = config.SourceConfig.filter((site) => !site.disabled);
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
|
|
||||||
// 共享状态
|
// 共享状态
|
||||||
let streamClosed = false;
|
let streamClosed = false;
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ function checkEnvironment(): void {
|
|||||||
if (!username || username.trim() === '') {
|
if (!username || username.trim() === '') {
|
||||||
console.error('❌ USERNAME 环境变量不得为空');
|
console.error('❌ USERNAME 环境变量不得为空');
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 PASSWORD
|
// 检查 PASSWORD
|
||||||
@@ -293,7 +293,7 @@ function checkEnvironment(): void {
|
|||||||
if (!password || password.trim() === '') {
|
if (!password || password.trim() === '') {
|
||||||
console.error('❌ PASSWORD 环境变量不得为空');
|
console.error('❌ PASSWORD 环境变量不得为空');
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查弱密码
|
// 检查弱密码
|
||||||
@@ -330,20 +330,20 @@ function checkEnvironment(): void {
|
|||||||
if (weakPasswords.includes(password.toLowerCase())) {
|
if (weakPasswords.includes(password.toLowerCase())) {
|
||||||
console.error(`❌ PASSWORD 不能使用常见弱密码: ${password}`);
|
console.error(`❌ PASSWORD 不能使用常见弱密码: ${password}`);
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
console.error('❌ PASSWORD 长度不能少于8位');
|
console.error('❌ PASSWORD 长度不能少于8位');
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查密码不能与用户名相同
|
// 检查密码不能与用户名相同
|
||||||
if (password.toLowerCase() === username.toLowerCase()) {
|
if (password.toLowerCase() === username.toLowerCase()) {
|
||||||
console.error('❌ PASSWORD 不能与 USERNAME 相同');
|
console.error('❌ PASSWORD 不能与 USERNAME 相同');
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 AUTH_TOKEN
|
// 检查 AUTH_TOKEN
|
||||||
@@ -351,7 +351,7 @@ function checkEnvironment(): void {
|
|||||||
if (!authToken || authToken.trim() === '') {
|
if (!authToken || authToken.trim() === '') {
|
||||||
console.error('❌ AUTH_TOKEN 不得为空');
|
console.error('❌ AUTH_TOKEN 不得为空');
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 AUTH_SERVER(可选,但如果设置了需要验证格式)
|
// 检查 AUTH_SERVER(可选,但如果设置了需要验证格式)
|
||||||
@@ -360,7 +360,7 @@ function checkEnvironment(): void {
|
|||||||
if (!authServer.startsWith('https://') && !authServer.startsWith('http://')) {
|
if (!authServer.startsWith('https://') && !authServer.startsWith('http://')) {
|
||||||
console.error('❌ AUTH_SERVER 必须以 http:// 或 https:// 开头');
|
console.error('❌ AUTH_SERVER 必须以 http:// 或 https:// 开头');
|
||||||
console.error('🚨 环境变量检查失败,服务器即将退出');
|
console.error('🚨 环境变量检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +377,7 @@ async function checkAuthentication(): Promise<void> {
|
|||||||
if (!authToken || !username || !password) {
|
if (!authToken || !username || !password) {
|
||||||
console.error('❌ 认证检查失败:缺少必需的环境变量');
|
console.error('❌ 认证检查失败:缺少必需的环境变量');
|
||||||
console.error('🚨 认证检查失败,服务器即将退出');
|
console.error('🚨 认证检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -419,7 +419,7 @@ async function checkAuthentication(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 认证流程失败:', error instanceof Error ? error.message : '未知错误');
|
console.error('❌ 认证流程失败:', error instanceof Error ? error.message : '未知错误');
|
||||||
console.error('🚨 认证检查失败,服务器即将退出');
|
console.error('🚨 认证检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@ function checkDatabaseConfig(): void {
|
|||||||
if (!allowedStorageTypes.includes(storageType)) {
|
if (!allowedStorageTypes.includes(storageType)) {
|
||||||
console.error(`❌ NEXT_PUBLIC_STORAGE_TYPE 必须是 ${allowedStorageTypes.join(', ')} 之一,当前值: ${storageType}`);
|
console.error(`❌ NEXT_PUBLIC_STORAGE_TYPE 必须是 ${allowedStorageTypes.join(', ')} 之一,当前值: ${storageType}`);
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据存储类型检查相应的环境变量
|
// 根据存储类型检查相应的环境变量
|
||||||
@@ -444,12 +444,12 @@ function checkDatabaseConfig(): void {
|
|||||||
if (!kvrocksUrl || kvrocksUrl.trim() === '') {
|
if (!kvrocksUrl || kvrocksUrl.trim() === '') {
|
||||||
console.error('❌ KVROCKS_URL 环境变量不得为空');
|
console.error('❌ KVROCKS_URL 环境变量不得为空');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
if (!kvrocksUrl.startsWith('redis://')) {
|
if (!kvrocksUrl.startsWith('redis://')) {
|
||||||
console.error('❌ KVROCKS_URL 必须以 redis:// 开头');
|
console.error('❌ KVROCKS_URL 必须以 redis:// 开头');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -460,18 +460,18 @@ function checkDatabaseConfig(): void {
|
|||||||
if (!upstashUrl || upstashUrl.trim() === '') {
|
if (!upstashUrl || upstashUrl.trim() === '') {
|
||||||
console.error('❌ UPSTASH_URL 环境变量不得为空');
|
console.error('❌ UPSTASH_URL 环境变量不得为空');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
if (!upstashUrl.startsWith('https://')) {
|
if (!upstashUrl.startsWith('https://')) {
|
||||||
console.error('❌ UPSTASH_URL 必须以 https:// 开头');
|
console.error('❌ UPSTASH_URL 必须以 https:// 开头');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!upstashToken || upstashToken.trim() === '') {
|
if (!upstashToken || upstashToken.trim() === '') {
|
||||||
console.error('❌ UPSTASH_TOKEN 环境变量不得为空');
|
console.error('❌ UPSTASH_TOKEN 环境变量不得为空');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -480,12 +480,12 @@ function checkDatabaseConfig(): void {
|
|||||||
if (!redisUrl || redisUrl.trim() === '') {
|
if (!redisUrl || redisUrl.trim() === '') {
|
||||||
console.error('❌ REDIS_URL 环境变量不得为空');
|
console.error('❌ REDIS_URL 环境变量不得为空');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
if (!redisUrl.startsWith('redis://')) {
|
if (!redisUrl.startsWith('redis://')) {
|
||||||
console.error('❌ REDIS_URL 必须以 redis:// 开头');
|
console.error('❌ REDIS_URL 必须以 redis:// 开头');
|
||||||
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
console.error('🚨 数据库配置检查失败,服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -540,7 +540,7 @@ export async function register() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('💥 启动检查过程中发生未预期错误:', error);
|
console.error('💥 启动检查过程中发生未预期错误:', error);
|
||||||
console.error('🚨 服务器即将退出');
|
console.error('🚨 服务器即将退出');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface AdminConfig {
|
|||||||
username: string;
|
username: string;
|
||||||
role: 'user' | 'admin' | 'owner';
|
role: 'user' | 'admin' | 'owner';
|
||||||
banned?: boolean;
|
banned?: boolean;
|
||||||
|
enabledApis?: string[]; // 为空则允许全部
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
SourceConfig: {
|
SourceConfig: {
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
// 过滤站长
|
// 过滤站长
|
||||||
|
const originOwnerCfg = adminConfig.UserConfig.Users.find((u) => u.username === ownerUser);
|
||||||
adminConfig.UserConfig.Users = adminConfig.UserConfig.Users.filter((user) => user.username !== ownerUser);
|
adminConfig.UserConfig.Users = adminConfig.UserConfig.Users.filter((user) => user.username !== ownerUser);
|
||||||
// 其他用户不得拥有 owner 权限
|
// 其他用户不得拥有 owner 权限
|
||||||
adminConfig.UserConfig.Users.forEach((user) => {
|
adminConfig.UserConfig.Users.forEach((user) => {
|
||||||
@@ -287,6 +288,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
|||||||
username: ownerUser!,
|
username: ownerUser!,
|
||||||
role: 'owner',
|
role: 'owner',
|
||||||
banned: false,
|
banned: false,
|
||||||
|
enabledApis: originOwnerCfg?.enabledApis || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 采集源去重
|
// 采集源去重
|
||||||
@@ -333,9 +335,15 @@ export async function getCacheTime(): Promise<number> {
|
|||||||
return config.SiteConfig.SiteInterfaceCacheTime || 7200;
|
return config.SiteConfig.SiteInterfaceCacheTime || 7200;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAvailableApiSites(): Promise<ApiSite[]> {
|
export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
return config.SourceConfig.filter((s) => !s.disabled).map((s) => ({
|
const allApiSites = config.SourceConfig.filter((s) => !s.disabled);
|
||||||
|
const userApiSites = user ? config.UserConfig.Users.find((u) => u.username === user)?.enabledApis || [] : [];
|
||||||
|
if (userApiSites.length === 0) {
|
||||||
|
return allApiSites;
|
||||||
|
}
|
||||||
|
const userApiSitesSet = new Set(userApiSites);
|
||||||
|
return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({
|
||||||
key: s.key,
|
key: s.key,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
api: s.api,
|
api: s.api,
|
||||||
|
|||||||
Reference in New Issue
Block a user