用户组增加权限控制
This commit is contained in:
@@ -57,6 +57,7 @@ public/workbox-*.js
|
|||||||
public/workbox-*.js.map
|
public/workbox-*.js.map
|
||||||
|
|
||||||
/.claude
|
/.claude
|
||||||
|
/.codex
|
||||||
/.vscode
|
/.vscode
|
||||||
# SQLite 开发数据库
|
# SQLite 开发数据库
|
||||||
.data/
|
.data/
|
||||||
|
|||||||
+140
-41
@@ -51,6 +51,7 @@ import { createPortal } from 'react-dom';
|
|||||||
|
|
||||||
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
|
import { FEATURE_PERMISSION_OPTIONS } from '@/lib/feature-permissions';
|
||||||
|
|
||||||
import AnimeSubscriptionComponent from '@/components/AnimeSubscriptionComponent';
|
import AnimeSubscriptionComponent from '@/components/AnimeSubscriptionComponent';
|
||||||
import CorrectDialog from '@/components/CorrectDialog';
|
import CorrectDialog from '@/components/CorrectDialog';
|
||||||
@@ -505,10 +506,12 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
const [newUserGroup, setNewUserGroup] = useState({
|
const [newUserGroup, setNewUserGroup] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
enabledApis: [] as string[],
|
enabledApis: [] as string[],
|
||||||
|
permissions: FEATURE_PERMISSION_OPTIONS.map((item) => item.key) as string[],
|
||||||
});
|
});
|
||||||
const [editingUserGroup, setEditingUserGroup] = useState<{
|
const [editingUserGroup, setEditingUserGroup] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
enabledApis: string[];
|
enabledApis: string[];
|
||||||
|
permissions: string[];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [showConfigureApisModal, setShowConfigureApisModal] = useState(false);
|
const [showConfigureApisModal, setShowConfigureApisModal] = useState(false);
|
||||||
const [selectedUser, setSelectedUser] = useState<{
|
const [selectedUser, setSelectedUser] = useState<{
|
||||||
@@ -577,7 +580,8 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
const handleUserGroupAction = async (
|
const handleUserGroupAction = async (
|
||||||
action: 'add' | 'edit' | 'delete',
|
action: 'add' | 'edit' | 'delete',
|
||||||
groupName: string,
|
groupName: string,
|
||||||
enabledApis?: string[]
|
enabledApis?: string[],
|
||||||
|
permissions?: string[]
|
||||||
) => {
|
) => {
|
||||||
return withLoading(`userGroup_${action}_${groupName}`, async () => {
|
return withLoading(`userGroup_${action}_${groupName}`, async () => {
|
||||||
try {
|
try {
|
||||||
@@ -589,6 +593,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
groupAction: action,
|
groupAction: action,
|
||||||
groupName,
|
groupName,
|
||||||
enabledApis,
|
enabledApis,
|
||||||
|
permissions,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -600,7 +605,11 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
await refreshConfig();
|
await refreshConfig();
|
||||||
|
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
setNewUserGroup({ name: '', enabledApis: [] });
|
setNewUserGroup({
|
||||||
|
name: '',
|
||||||
|
enabledApis: [],
|
||||||
|
permissions: FEATURE_PERMISSION_OPTIONS.map((item) => item.key),
|
||||||
|
});
|
||||||
setShowAddUserGroupForm(false);
|
setShowAddUserGroupForm(false);
|
||||||
} else if (action === 'edit') {
|
} else if (action === 'edit') {
|
||||||
setEditingUserGroup(null);
|
setEditingUserGroup(null);
|
||||||
@@ -624,7 +633,12 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
|
|
||||||
const handleAddUserGroup = () => {
|
const handleAddUserGroup = () => {
|
||||||
if (!newUserGroup.name.trim()) return;
|
if (!newUserGroup.name.trim()) return;
|
||||||
handleUserGroupAction('add', newUserGroup.name, newUserGroup.enabledApis);
|
handleUserGroupAction(
|
||||||
|
'add',
|
||||||
|
newUserGroup.name,
|
||||||
|
newUserGroup.enabledApis,
|
||||||
|
newUserGroup.permissions
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditUserGroup = () => {
|
const handleEditUserGroup = () => {
|
||||||
@@ -632,7 +646,8 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
handleUserGroupAction(
|
handleUserGroupAction(
|
||||||
'edit',
|
'edit',
|
||||||
editingUserGroup.name,
|
editingUserGroup.name,
|
||||||
editingUserGroup.enabledApis
|
editingUserGroup.enabledApis,
|
||||||
|
editingUserGroup.permissions
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -668,8 +683,12 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
const handleStartEditUserGroup = (group: {
|
const handleStartEditUserGroup = (group: {
|
||||||
name: string;
|
name: string;
|
||||||
enabledApis: string[];
|
enabledApis: string[];
|
||||||
|
permissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
setEditingUserGroup({ ...group });
|
setEditingUserGroup({
|
||||||
|
...group,
|
||||||
|
permissions: group.permissions || [],
|
||||||
|
});
|
||||||
setShowEditUserGroupForm(true);
|
setShowEditUserGroupForm(true);
|
||||||
setShowAddUserGroupForm(false);
|
setShowAddUserGroupForm(false);
|
||||||
};
|
};
|
||||||
@@ -1101,6 +1120,9 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
可用视频源
|
可用视频源
|
||||||
</th>
|
</th>
|
||||||
|
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
|
功能权限
|
||||||
|
</th>
|
||||||
<th className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
<th className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
操作
|
操作
|
||||||
</th>
|
</th>
|
||||||
@@ -1124,6 +1146,13 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className='px-6 py-4 whitespace-nowrap'>
|
||||||
|
<span className='text-sm text-gray-900 dark:text-gray-100'>
|
||||||
|
{group.permissions && group.permissions.length > 0
|
||||||
|
? `${group.permissions.length} 项`
|
||||||
|
: '无'}
|
||||||
|
</span>
|
||||||
|
</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'>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleStartEditUserGroup(group)}
|
onClick={() => handleStartEditUserGroup(group)}
|
||||||
@@ -1148,7 +1177,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
{userGroups.length === 0 && (
|
{userGroups.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={3}
|
colSpan={4}
|
||||||
className='px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400'
|
className='px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400'
|
||||||
>
|
>
|
||||||
暂无用户组,请添加用户组来管理用户权限
|
暂无用户组,请添加用户组来管理用户权限
|
||||||
@@ -1925,7 +1954,11 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
|
className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAddUserGroupForm(false);
|
setShowAddUserGroupForm(false);
|
||||||
setNewUserGroup({ name: '', enabledApis: [] });
|
setNewUserGroup({
|
||||||
|
name: '',
|
||||||
|
enabledApis: [],
|
||||||
|
permissions: FEATURE_PERMISSION_OPTIONS.map((item) => item.key),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1940,7 +1973,11 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAddUserGroupForm(false);
|
setShowAddUserGroupForm(false);
|
||||||
setNewUserGroup({ name: '', enabledApis: [] });
|
setNewUserGroup({
|
||||||
|
name: '',
|
||||||
|
enabledApis: [],
|
||||||
|
permissions: FEATURE_PERMISSION_OPTIONS.map((item) => item.key),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
|
className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
|
||||||
>
|
>
|
||||||
@@ -1980,6 +2017,47 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
|
||||||
|
功能权限
|
||||||
|
</label>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-3'>
|
||||||
|
{FEATURE_PERMISSION_OPTIONS.map((permission) => (
|
||||||
|
<label
|
||||||
|
key={permission.key}
|
||||||
|
className='flex items-start 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={newUserGroup.permissions.includes(permission.key)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setNewUserGroup((prev) => ({
|
||||||
|
...prev,
|
||||||
|
permissions: [...prev.permissions, permission.key],
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setNewUserGroup((prev) => ({
|
||||||
|
...prev,
|
||||||
|
permissions: prev.permissions.filter((item) => item !== permission.key),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='mt-0.5 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'>
|
||||||
|
{permission.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
{permission.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 可用视频源 */}
|
{/* 可用视频源 */}
|
||||||
<div>
|
<div>
|
||||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
|
||||||
@@ -2066,7 +2144,11 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAddUserGroupForm(false);
|
setShowAddUserGroupForm(false);
|
||||||
setNewUserGroup({ name: '', enabledApis: [] });
|
setNewUserGroup({
|
||||||
|
name: '',
|
||||||
|
enabledApis: [],
|
||||||
|
permissions: FEATURE_PERMISSION_OPTIONS.map((item) => item.key),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.secondary}`}
|
className={`px-6 py-2.5 text-sm font-medium ${buttonStyles.secondary}`}
|
||||||
>
|
>
|
||||||
@@ -2228,6 +2310,55 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
|
||||||
|
功能权限
|
||||||
|
</label>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-3'>
|
||||||
|
{FEATURE_PERMISSION_OPTIONS.map((permission) => (
|
||||||
|
<label
|
||||||
|
key={permission.key}
|
||||||
|
className='flex items-start 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={editingUserGroup.permissions.includes(permission.key)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setEditingUserGroup((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
permissions: [...prev.permissions, permission.key],
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setEditingUserGroup((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
permissions: prev.permissions.filter((item) => item !== permission.key),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='mt-0.5 rounded border-gray-300 text-purple-600 focus:ring-purple-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'>
|
||||||
|
{permission.label}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
{permission.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<div className='flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
<div className='flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||||
<button
|
<button
|
||||||
@@ -11290,9 +11421,6 @@ const AIConfigComponent = ({
|
|||||||
const [enablePlayPageEntry, setEnablePlayPageEntry] = useState(true);
|
const [enablePlayPageEntry, setEnablePlayPageEntry] = useState(true);
|
||||||
const [enableAIComments, setEnableAIComments] = useState(false);
|
const [enableAIComments, setEnableAIComments] = useState(false);
|
||||||
|
|
||||||
// 权限控制
|
|
||||||
const [allowRegularUsers, setAllowRegularUsers] = useState(true);
|
|
||||||
|
|
||||||
// 高级设置
|
// 高级设置
|
||||||
const [temperature, setTemperature] = useState(0.7);
|
const [temperature, setTemperature] = useState(0.7);
|
||||||
const [maxTokens, setMaxTokens] = useState(1000);
|
const [maxTokens, setMaxTokens] = useState(1000);
|
||||||
@@ -11320,7 +11448,6 @@ const AIConfigComponent = ({
|
|||||||
setEnableVideoCardEntry(config.AIConfig.EnableVideoCardEntry !== false);
|
setEnableVideoCardEntry(config.AIConfig.EnableVideoCardEntry !== false);
|
||||||
setEnablePlayPageEntry(config.AIConfig.EnablePlayPageEntry !== false);
|
setEnablePlayPageEntry(config.AIConfig.EnablePlayPageEntry !== false);
|
||||||
setEnableAIComments(config.AIConfig.EnableAIComments || false);
|
setEnableAIComments(config.AIConfig.EnableAIComments || false);
|
||||||
setAllowRegularUsers(config.AIConfig.AllowRegularUsers !== false);
|
|
||||||
setTemperature(config.AIConfig.Temperature ?? 0.7);
|
setTemperature(config.AIConfig.Temperature ?? 0.7);
|
||||||
setMaxTokens(config.AIConfig.MaxTokens ?? 1000);
|
setMaxTokens(config.AIConfig.MaxTokens ?? 1000);
|
||||||
setSystemPrompt(config.AIConfig.SystemPrompt || '');
|
setSystemPrompt(config.AIConfig.SystemPrompt || '');
|
||||||
@@ -11354,7 +11481,6 @@ const AIConfigComponent = ({
|
|||||||
EnableVideoCardEntry: enableVideoCardEntry,
|
EnableVideoCardEntry: enableVideoCardEntry,
|
||||||
EnablePlayPageEntry: enablePlayPageEntry,
|
EnablePlayPageEntry: enablePlayPageEntry,
|
||||||
EnableAIComments: enableAIComments,
|
EnableAIComments: enableAIComments,
|
||||||
AllowRegularUsers: allowRegularUsers,
|
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
MaxTokens: maxTokens,
|
MaxTokens: maxTokens,
|
||||||
SystemPrompt: systemPrompt,
|
SystemPrompt: systemPrompt,
|
||||||
@@ -11647,33 +11773,6 @@ const AIConfigComponent = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 权限控制 */}
|
|
||||||
<div className='space-y-3 p-4 border border-gray-200 dark:border-gray-700 rounded-lg'>
|
|
||||||
<h4 className='text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3'>
|
|
||||||
权限控制
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between py-2'>
|
|
||||||
<div>
|
|
||||||
<div className='text-sm font-medium text-gray-900 dark:text-gray-100'>
|
|
||||||
允许普通用户使用
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-gray-500 dark:text-gray-400'>
|
|
||||||
关闭后仅站长和管理员可使用AI问片功能
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label className='relative inline-flex items-center cursor-pointer'>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
checked={allowRegularUsers}
|
|
||||||
onChange={(e) => setAllowRegularUsers(e.target.checked)}
|
|
||||||
className='sr-only peer'
|
|
||||||
/>
|
|
||||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-yellow-300 dark:peer-focus:ring-yellow-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 高级设置 */}
|
{/* 高级设置 */}
|
||||||
<details className='p-4 border border-gray-200 dark:border-gray-700 rounded-lg'>
|
<details className='p-4 border border-gray-200 dark:border-gray-700 rounded-lg'>
|
||||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { parseStringPromise } from 'xml2js';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
const authInfo = getAuthInfoFromCookie(req);
|
const authInfo = getAuthInfoFromCookie(req);
|
||||||
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
|
if (!authInfo?.username || !(await hasFeaturePermission(authInfo.username, 'magnet_search'))) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '无权限访问' },
|
{ error: '无权限访问' },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { parseStringPromise } from 'xml2js';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export const runtime = 'nodejs';
|
|||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authInfo = getAuthInfoFromCookie(req);
|
const authInfo = getAuthInfoFromCookie(req);
|
||||||
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
|
if (!authInfo?.username || !(await hasFeaturePermission(authInfo.username, 'magnet_search'))) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '无权限访问' },
|
{ error: '无权限访问' },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
const authInfo = getAuthInfoFromCookie(req);
|
const authInfo = getAuthInfoFromCookie(req);
|
||||||
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
|
if (!authInfo?.username || !(await hasFeaturePermission(authInfo.username, 'magnet_save_private_library'))) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '无权限访问' },
|
{ error: '无权限访问' },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { parseStringPromise } from 'xml2js';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
const authInfo = getAuthInfoFromCookie(req);
|
const authInfo = getAuthInfoFromCookie(req);
|
||||||
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
|
if (!authInfo?.username || !(await hasFeaturePermission(authInfo.username, 'magnet_search'))) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '无权限访问' },
|
{ error: '无权限访问' },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export async function POST(request: NextRequest) {
|
|||||||
EnableVideoCardEntry,
|
EnableVideoCardEntry,
|
||||||
EnablePlayPageEntry,
|
EnablePlayPageEntry,
|
||||||
EnableAIComments,
|
EnableAIComments,
|
||||||
AllowRegularUsers,
|
|
||||||
Temperature,
|
Temperature,
|
||||||
MaxTokens,
|
MaxTokens,
|
||||||
SystemPrompt,
|
SystemPrompt,
|
||||||
@@ -95,7 +94,6 @@ export async function POST(request: NextRequest) {
|
|||||||
EnableVideoCardEntry: boolean;
|
EnableVideoCardEntry: boolean;
|
||||||
EnablePlayPageEntry: boolean;
|
EnablePlayPageEntry: boolean;
|
||||||
EnableAIComments: boolean;
|
EnableAIComments: boolean;
|
||||||
AllowRegularUsers: boolean;
|
|
||||||
Temperature?: number;
|
Temperature?: number;
|
||||||
MaxTokens?: number;
|
MaxTokens?: number;
|
||||||
SystemPrompt?: string;
|
SystemPrompt?: string;
|
||||||
@@ -135,7 +133,6 @@ export async function POST(request: NextRequest) {
|
|||||||
typeof EnableVideoCardEntry !== 'boolean' ||
|
typeof EnableVideoCardEntry !== 'boolean' ||
|
||||||
typeof EnablePlayPageEntry !== 'boolean' ||
|
typeof EnablePlayPageEntry !== 'boolean' ||
|
||||||
typeof EnableAIComments !== 'boolean' ||
|
typeof EnableAIComments !== 'boolean' ||
|
||||||
typeof AllowRegularUsers !== 'boolean' ||
|
|
||||||
(Temperature !== undefined && typeof Temperature !== 'number') ||
|
(Temperature !== undefined && typeof Temperature !== 'number') ||
|
||||||
(MaxTokens !== undefined && typeof MaxTokens !== 'number') ||
|
(MaxTokens !== undefined && typeof MaxTokens !== 'number') ||
|
||||||
(SystemPrompt !== undefined && typeof SystemPrompt !== 'string') ||
|
(SystemPrompt !== undefined && typeof SystemPrompt !== 'string') ||
|
||||||
@@ -185,7 +182,6 @@ export async function POST(request: NextRequest) {
|
|||||||
EnableVideoCardEntry,
|
EnableVideoCardEntry,
|
||||||
EnablePlayPageEntry,
|
EnablePlayPageEntry,
|
||||||
EnableAIComments,
|
EnableAIComments,
|
||||||
AllowRegularUsers,
|
|
||||||
Temperature,
|
Temperature,
|
||||||
MaxTokens,
|
MaxTokens,
|
||||||
SystemPrompt,
|
SystemPrompt,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { sanitizeFeaturePermissions } from '@/lib/feature-permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -356,11 +357,13 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
case 'userGroup': {
|
case 'userGroup': {
|
||||||
// 用户组管理操作
|
// 用户组管理操作
|
||||||
const { groupAction, groupName, enabledApis } = body as {
|
const { groupAction, groupName, enabledApis, permissions } = body as {
|
||||||
groupAction: 'add' | 'edit' | 'delete';
|
groupAction: 'add' | 'edit' | 'delete';
|
||||||
groupName: string;
|
groupName: string;
|
||||||
enabledApis?: string[];
|
enabledApis?: string[];
|
||||||
|
permissions?: string[];
|
||||||
};
|
};
|
||||||
|
const normalizedPermissions = sanitizeFeaturePermissions(permissions);
|
||||||
|
|
||||||
if (!adminConfig.UserConfig.Tags) {
|
if (!adminConfig.UserConfig.Tags) {
|
||||||
adminConfig.UserConfig.Tags = [];
|
adminConfig.UserConfig.Tags = [];
|
||||||
@@ -375,6 +378,7 @@ export async function POST(request: NextRequest) {
|
|||||||
adminConfig.UserConfig.Tags.push({
|
adminConfig.UserConfig.Tags.push({
|
||||||
name: groupName,
|
name: groupName,
|
||||||
enabledApis: enabledApis || [],
|
enabledApis: enabledApis || [],
|
||||||
|
permissions: normalizedPermissions,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -384,6 +388,7 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '用户组不存在' }, { status: 404 });
|
return NextResponse.json({ error: '用户组不存在' }, { status: 404 });
|
||||||
}
|
}
|
||||||
adminConfig.UserConfig.Tags[groupIndex].enabledApis = enabledApis || [];
|
adminConfig.UserConfig.Tags[groupIndex].enabledApis = enabledApis || [];
|
||||||
|
adminConfig.UserConfig.Tags[groupIndex].permissions = normalizedPermissions;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@/lib/ai-orchestrator';
|
} from '@/lib/ai-orchestrator';
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -212,6 +212,10 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await hasFeaturePermission(authInfo.username, 'ai_ask'))) {
|
||||||
|
return NextResponse.json({ error: '无权限使用 AI 问片功能' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 获取AI配置
|
// 2. 获取AI配置
|
||||||
const adminConfig = await getConfig();
|
const adminConfig = await getConfig();
|
||||||
const aiConfig = adminConfig.AIConfig;
|
const aiConfig = adminConfig.AIConfig;
|
||||||
@@ -223,23 +227,7 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 权限检查:如果不允许普通用户使用,检查用户角色
|
// 3. 解析请求参数
|
||||||
if (!aiConfig.AllowRegularUsers) {
|
|
||||||
const username = authInfo.username;
|
|
||||||
// 站长始终有权限
|
|
||||||
if (username !== process.env.USERNAME) {
|
|
||||||
// 检查是否为管理员
|
|
||||||
const userInfo = await db.getUserInfoV2(username);
|
|
||||||
if (!userInfo || (userInfo.role !== 'admin' && userInfo.role !== 'owner') || userInfo.banned) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: '该功能仅限站长和管理员使用' },
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 解析请求参数
|
|
||||||
const body = (await request.json()) as ChatRequest;
|
const body = (await request.json()) as ChatRequest;
|
||||||
const { message, context, history = [] } = body;
|
const { message, context, history = [] } = body;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { EmbyClient } from '@/lib/emby.client';
|
import { EmbyClient } from '@/lib/emby.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -45,7 +46,8 @@ export async function GET(
|
|||||||
if (username) {
|
if (username) {
|
||||||
// 检查用户是否被封禁
|
// 检查用户是否被封禁
|
||||||
const userInfo = await db.getUserInfoV2(username);
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
if (userInfo && !userInfo.banned) {
|
const allowed = await hasFeaturePermission(username, 'emby');
|
||||||
|
if (userInfo && !userInfo.banned && allowed) {
|
||||||
isValidToken = true;
|
isValidToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
import { getProxyToken } from '@/lib/emby-token';
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'emby', '无权限访问 Emby');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 获取Emby客户端
|
// 获取Emby客户端
|
||||||
const client = await embyManager.getClient(embyKey);
|
const client = await embyManager.getClient(embyKey);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -54,14 +55,18 @@ export async function GET(
|
|||||||
if (username) {
|
if (username) {
|
||||||
// 检查用户是否被封禁
|
// 检查用户是否被封禁
|
||||||
const userInfo = await db.getUserInfoV2(username);
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
if (userInfo && !userInfo.banned) {
|
const allowed = await hasFeaturePermission(username, 'emby');
|
||||||
|
if (userInfo && !userInfo.banned && allowed) {
|
||||||
hasValidToken = true;
|
hasValidToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户登录
|
// 验证用户登录
|
||||||
const hasValidAuth = authInfo && authInfo.username;
|
const hasValidAuth = !!(
|
||||||
|
authInfo?.username &&
|
||||||
|
(await hasFeaturePermission(authInfo.username, 'emby'))
|
||||||
|
);
|
||||||
|
|
||||||
// 两者至少满足其一
|
// 两者至少满足其一
|
||||||
if (!hasValidToken && !hasValidAuth) {
|
if (!hasValidToken && !hasValidAuth) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
import { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
import { getProxyToken } from '@/lib/emby-token';
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const sortOrder = searchParams.get('sortOrder') || 'Ascending';
|
const sortOrder = searchParams.get('sortOrder') || 'Ascending';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'emby', '无权限访问 Emby');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 判断是否是默认排序(只有默认排序才使用缓存)
|
// 判断是否是默认排序(只有默认排序才使用缓存)
|
||||||
const isDefaultSort = sortBy === 'SortName' && sortOrder === 'Ascending';
|
const isDefaultSort = sortBy === 'SortName' && sortOrder === 'Ascending';
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -55,14 +56,18 @@ export async function GET(
|
|||||||
if (username) {
|
if (username) {
|
||||||
// 检查用户是否被封禁
|
// 检查用户是否被封禁
|
||||||
const userInfo = await db.getUserInfoV2(username);
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
if (userInfo && !userInfo.banned) {
|
const allowed = await hasFeaturePermission(username, 'emby');
|
||||||
|
if (userInfo && !userInfo.banned && allowed) {
|
||||||
hasValidToken = true;
|
hasValidToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户登录
|
// 验证用户登录
|
||||||
const hasValidAuth = authInfo && authInfo.username;
|
const hasValidAuth = !!(
|
||||||
|
authInfo?.username &&
|
||||||
|
(await hasFeaturePermission(authInfo.username, 'emby'))
|
||||||
|
);
|
||||||
|
|
||||||
// 两者至少满足其一
|
// 两者至少满足其一
|
||||||
if (!hasValidToken && !hasValidAuth) {
|
if (!hasValidToken && !hasValidAuth) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic'; // 禁用缓存
|
export const dynamic = 'force-dynamic'; // 禁用缓存
|
||||||
@@ -8,8 +9,10 @@ export const dynamic = 'force-dynamic'; // 禁用缓存
|
|||||||
/**
|
/**
|
||||||
* 获取所有启用的Emby源列表
|
* 获取所有启用的Emby源列表
|
||||||
*/
|
*/
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'emby', '无权限访问 Emby');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const sources = await embyManager.getEnabledSources();
|
const sources = await embyManager.getEnabledSources();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getCachedEmbyViews, setCachedEmbyViews } from '@/lib/emby-cache';
|
import { getCachedEmbyViews, setCachedEmbyViews } from '@/lib/emby-cache';
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'emby', '无权限访问 Emby');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const embyKey = searchParams.get('embyKey') || undefined;
|
const embyKey = searchParams.get('embyKey') || undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getCachedLiveChannels } from '@/lib/live';
|
import { getCachedLiveChannels } from '@/lib/live';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'live', '无权限访问电视直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const sourceKey = searchParams.get('source');
|
const sourceKey = searchParams.get('source');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getCachedLiveChannels } from '@/lib/live';
|
import { getCachedLiveChannels } from '@/lib/live';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'live', '无权限访问电视直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const sourceKey = searchParams.get('source');
|
const sourceKey = searchParams.get('source');
|
||||||
const tvgId = searchParams.get('tvgId');
|
const tvgId = searchParams.get('tvgId');
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'live', '无权限访问电视直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const url = searchParams.get('url');
|
const url = searchParams.get('url');
|
||||||
const source = searchParams.get('moontv-source');
|
const source = searchParams.get('moontv-source');
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
console.log(request.url)
|
console.log(request.url)
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'live', '无权限访问电视直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export async function getAuthorizedUsername(request: NextRequest): Promise<string | NextResponse> {
|
export async function getAuthorizedUsername(request: NextRequest): Promise<string | NextResponse> {
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
@@ -19,5 +20,10 @@ export async function getAuthorizedUsername(request: NextRequest): Promise<strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowed = await hasFeaturePermission(authInfo.username, 'manga');
|
||||||
|
if (!allowed) {
|
||||||
|
return NextResponse.json({ error: '无权限访问漫画功能' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
return authInfo.username;
|
return authInfo.username;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -30,6 +31,8 @@ async function getOpenListClient(): Promise<OpenListClient | null> {
|
|||||||
// 代理OpenList缓存的音频文件
|
// 代理OpenList缓存的音频文件
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const platform = searchParams.get('platform');
|
const platform = searchParams.get('platform');
|
||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import { randomUUID } from 'crypto';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
// GET - 获取用户的所有歌单
|
// GET - 获取用户的所有歌单
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -43,6 +46,8 @@ export async function GET(request: NextRequest) {
|
|||||||
// POST - 创建新歌单
|
// POST - 创建新歌单
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -93,6 +98,8 @@ export async function POST(request: NextRequest) {
|
|||||||
// PUT - 更新歌单信息
|
// PUT - 更新歌单信息
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -151,6 +158,8 @@ export async function PUT(request: NextRequest) {
|
|||||||
// DELETE - 删除歌单
|
// DELETE - 删除歌单
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
// GET - 获取歌单中的所有歌曲
|
// GET - 获取歌单中的所有歌曲
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -61,6 +64,8 @@ export async function GET(request: NextRequest) {
|
|||||||
// POST - 添加歌曲到歌单
|
// POST - 添加歌曲到歌单
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -136,6 +141,8 @@ export async function POST(request: NextRequest) {
|
|||||||
// DELETE - 从歌单中移除歌曲
|
// DELETE - 从歌单中移除歌曲
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { MusicPlayRecord } from '@/lib/db.client';
|
import { MusicPlayRecord } from '@/lib/db.client';
|
||||||
import { getCachedSongs, setCachedSong } from '@/lib/music-song-cache';
|
import { getCachedSongs, setCachedSong } from '@/lib/music-song-cache';
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ export const runtime = 'nodejs';
|
|||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -63,6 +66,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
@@ -181,6 +186,8 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 从 cookie 获取用户信息
|
// 从 cookie 获取用户信息
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
// 代理音频流
|
// 代理音频流
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const url = searchParams.get('url');
|
const url = searchParams.get('url');
|
||||||
|
|
||||||
|
|||||||
+25
-20
@@ -3,6 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -20,19 +21,19 @@ const serverCache = {
|
|||||||
// 正在下载的音频任务追踪(防止重复下载)
|
// 正在下载的音频任务追踪(防止重复下载)
|
||||||
const downloadingTasks = new Map<string, Promise<void>>();
|
const downloadingTasks = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
// 获取 TuneHub 配置
|
// 获取音乐服务配置
|
||||||
async function getTuneHubConfig() {
|
async function getMusicServiceConfig() {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const musicConfig = config?.MusicConfig;
|
const musicConfig = config?.MusicConfig;
|
||||||
|
|
||||||
const enabled = musicConfig?.TuneHubEnabled ?? false;
|
const enabled = musicConfig?.Enabled ?? false;
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
musicConfig?.TuneHubBaseUrl ||
|
musicConfig?.BaseUrl ||
|
||||||
process.env.TUNEHUB_BASE_URL ||
|
process.env.MUSIC_V2_BASE_URL ||
|
||||||
'https://tunehub.sayqz.com/api';
|
'';
|
||||||
const apiKey = musicConfig?.TuneHubApiKey || process.env.TUNEHUB_API_KEY || '';
|
const token = musicConfig?.Token || process.env.MUSIC_V2_TOKEN || '';
|
||||||
|
|
||||||
return { enabled, baseUrl, apiKey, musicConfig };
|
return { enabled, baseUrl, token, musicConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 OpenList 客户端
|
// 获取 OpenList 客户端
|
||||||
@@ -135,7 +136,7 @@ async function replaceAudioUrlsWithOpenList(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TuneHub 返回的数据结构是 { code: 0, data: { data: [...], total: 1 } }
|
// 音乐服务返回的数据结构是 { code: 0, data: { data: [...], total: 1 } }
|
||||||
// 需要提取内层的 data 数组
|
// 需要提取内层的 data 数组
|
||||||
const songsData = data.data.data || data.data;
|
const songsData = data.data.data || data.data;
|
||||||
const songs = Array.isArray(songsData) ? songsData : [songsData];
|
const songs = Array.isArray(songsData) ? songsData : [songsData];
|
||||||
@@ -205,7 +206,7 @@ async function proxyRequest(
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('TuneHub API 请求失败:', error);
|
console.error('Music API 请求失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +395,9 @@ async function executeMethod(
|
|||||||
// GET 请求处理
|
// GET 请求处理
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { enabled, baseUrl } = await getTuneHubConfig();
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
|
const { enabled, baseUrl } = await getMusicServiceConfig();
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -544,7 +547,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// POST 请求处理(用于解析歌曲)
|
// POST 请求处理(用于解析歌曲)
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { enabled, baseUrl, apiKey } = await getTuneHubConfig();
|
const authResult = await requireFeaturePermission(request, 'music', '无权限访问音乐功能');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
|
const { enabled, baseUrl, token } = await getMusicServiceConfig();
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -565,13 +570,13 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'parse': {
|
case 'parse': {
|
||||||
// 解析歌曲(需要 API Key)
|
// 解析歌曲(需要 Token)
|
||||||
if (!apiKey) {
|
if (!token) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
code: -1,
|
code: -1,
|
||||||
error: '未配置 TuneHub API Key',
|
error: '未配置音乐服务 Token',
|
||||||
message: '未配置 TuneHub API Key'
|
message: '未配置音乐服务 Token'
|
||||||
},
|
},
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
);
|
);
|
||||||
@@ -650,17 +655,17 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// OpenList 缓存未命中,继续调用 TuneHub
|
// OpenList 缓存未命中,继续调用音乐服务
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 调用 TuneHub API 解析
|
// 4. 调用音乐服务解析
|
||||||
try {
|
try {
|
||||||
const response = await proxyRequest(`${baseUrl}/v1/parse`, {
|
const response = await proxyRequest(`${baseUrl}/v1/parse`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-Key': apiKey,
|
'X-API-Key': token,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
platform,
|
platform,
|
||||||
@@ -671,7 +676,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 如果 TuneHub 返回错误,包装成统一格式
|
// 如果音乐服务返回错误,包装成统一格式
|
||||||
if (!response.ok || data.code !== 0) {
|
if (!response.ok || data.code !== 0) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
code: data.code || -1,
|
code: data.code || -1,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { createQuarkInstantPlayFolder } from '@/lib/netdisk/quark.client';
|
import { createQuarkInstantPlayFolder } from '@/lib/netdisk/quark.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
import { base58Encode } from '@/lib/utils';
|
import { base58Encode } from '@/lib/utils';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -21,6 +22,9 @@ export async function POST(request: NextRequest) {
|
|||||||
if (!authInfo?.username) {
|
if (!authInfo?.username) {
|
||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
if (!(await hasFeaturePermission(authInfo.username, 'netdisk_temp_play'))) {
|
||||||
|
return NextResponse.json({ error: '无权限使用临时播放' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const { shareUrl, passcode, title } = await request.json();
|
const { shareUrl, passcode, title } = await request.json();
|
||||||
if (!shareUrl) {
|
if (!shareUrl) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { transferQuarkShare } from '@/lib/netdisk/quark.client';
|
import { transferQuarkShare } from '@/lib/netdisk/quark.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ export async function POST(request: NextRequest) {
|
|||||||
if (!authInfo?.username) {
|
if (!authInfo?.username) {
|
||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
if (!(await hasFeaturePermission(authInfo.username, 'netdisk_transfer'))) {
|
||||||
|
return NextResponse.json({ error: '无权限使用网盘转存' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const { shareUrl, passcode } = await request.json();
|
const { shareUrl, passcode } = await request.json();
|
||||||
if (!shareUrl) {
|
if (!shareUrl) {
|
||||||
|
|||||||
@@ -3,6 +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 { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -13,6 +14,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 权限检查
|
// 权限检查
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -44,7 +45,8 @@ export async function GET(
|
|||||||
if (username) {
|
if (username) {
|
||||||
// 检查用户是否被封禁
|
// 检查用户是否被封禁
|
||||||
const userInfo = await db.getUserInfoV2(username);
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
if (userInfo && !userInfo.banned) {
|
const allowed = await hasFeaturePermission(username, 'private_library');
|
||||||
|
if (userInfo && !userInfo.banned && allowed) {
|
||||||
isValidToken = true;
|
isValidToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
invalidateMetaInfoCache,
|
invalidateMetaInfoCache,
|
||||||
@@ -19,6 +20,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 权限检查
|
// 权限检查
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
import {
|
import {
|
||||||
getCachedVideoInfo,
|
getCachedVideoInfo,
|
||||||
@@ -20,6 +21,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -39,14 +40,18 @@ export async function GET(
|
|||||||
if (username) {
|
if (username) {
|
||||||
// 检查用户是否被封禁
|
// 检查用户是否被封禁
|
||||||
const userInfo = await db.getUserInfoV2(username);
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
if (userInfo && !userInfo.banned) {
|
const allowed = await hasFeaturePermission(username, 'private_library');
|
||||||
|
if (userInfo && !userInfo.banned && allowed) {
|
||||||
hasValidToken = true;
|
hasValidToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户登录
|
// 验证用户登录
|
||||||
const hasValidAuth = authInfo && authInfo.username;
|
const hasValidAuth = !!(
|
||||||
|
authInfo?.username &&
|
||||||
|
(await hasFeaturePermission(authInfo.username, 'private_library'))
|
||||||
|
);
|
||||||
|
|
||||||
// 两者至少满足其一
|
// 两者至少满足其一
|
||||||
if (!hasValidToken && !hasValidAuth) {
|
if (!hasValidToken && !hasValidAuth) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -65,6 +66,8 @@ async function getFinalUrl(url: string, maxRedirects = 5): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
import { invalidateVideoInfoCache } from '@/lib/openlist-cache';
|
import { invalidateVideoInfoCache } from '@/lib/openlist-cache';
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { startOpenListRefresh } from '@/lib/openlist-refresh';
|
import { startOpenListRefresh } from '@/lib/openlist-refresh';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -14,6 +15,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
// 权限检查
|
// 权限检查
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
|
|||||||
@@ -3,6 +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 { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { getScanTask } from '@/lib/scan-task';
|
import { getScanTask } from '@/lib/scan-task';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -13,6 +14,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'private_library', '无权限访问私人影库');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { PansouLink, searchPansou } from '@/lib/pansou.client';
|
import { PansouLink, searchPansou } from '@/lib/pansou.client';
|
||||||
|
|
||||||
@@ -9,6 +10,13 @@ export const runtime = 'nodejs';
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(
|
||||||
|
request,
|
||||||
|
'netdisk_search',
|
||||||
|
'无权限使用网盘搜索'
|
||||||
|
);
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { keyword } = body;
|
const { keyword } = body;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getCachedLiveChannels } from '@/lib/live';
|
import { getCachedLiveChannels } from '@/lib/live';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -76,7 +77,12 @@ export async function GET(request: NextRequest) {
|
|||||||
const apiSites = await getAvailableApiSites(username);
|
const apiSites = await getAvailableApiSites(username);
|
||||||
|
|
||||||
// 获取直播源
|
// 获取直播源
|
||||||
const liveConfig = config.LiveConfig?.filter(live => !live.disabled) || [];
|
const canAccessLive = isGlobalToken || !username
|
||||||
|
? true
|
||||||
|
: await hasFeaturePermission(username, 'live');
|
||||||
|
const liveConfig = canAccessLive
|
||||||
|
? config.LiveConfig?.filter(live => !live.disabled) || []
|
||||||
|
: [];
|
||||||
|
|
||||||
// 获取当前请求的 origin,用于构建代理链接
|
// 获取当前请求的 origin,用于构建代理链接
|
||||||
// 优先级:SITE_BASE 环境变量 > origin 参数 > 从请求头构建
|
// 优先级:SITE_BASE 环境变量 > origin 参数 > 从请求头构建
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
function getBaseUrl(url: string): string {
|
function getBaseUrl(url: string): string {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const pathParts = urlObj.pathname.split('/');
|
const pathParts = urlObj.pathname.split('/');
|
||||||
@@ -38,6 +40,8 @@ function processM3u8Content(content: string, baseUrl: string): string {
|
|||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'web_live', '无权限访问网络直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const url = searchParams.get('url');
|
const url = searchParams.get('url');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'; // 禁用缓存
|
export const dynamic = 'force-dynamic'; // 禁用缓存
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'web_live', '无权限访问网络直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (!config?.WebLiveConfig) {
|
if (!config?.WebLiveConfig) {
|
||||||
return NextResponse.json([]);
|
return NextResponse.json([]);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
function getAntiCode(oldAntiCode: string, streamName: string): string {
|
function getAntiCode(oldAntiCode: string, streamName: string): string {
|
||||||
const paramsT = 100;
|
const paramsT = 100;
|
||||||
const sdkVersion = 2403051612;
|
const sdkVersion = 2403051612;
|
||||||
@@ -206,6 +208,8 @@ async function getDouyinStream(roomId: string) {
|
|||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'web_live', '无权限访问网络直播');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const platform = searchParams.get('platform');
|
const platform = searchParams.get('platform');
|
||||||
const roomId = searchParams.get('roomId');
|
const roomId = searchParams.get('roomId');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -14,6 +15,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'xiaoya', '无权限访问小雅');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -66,6 +67,8 @@ async function getFinalUrl(url: string, maxRedirects = 5): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'xiaoya', '无权限访问小雅');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { requireFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ export const runtime = 'nodejs';
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authResult = await requireFeaturePermission(request, 'xiaoya', '无权限访问小雅');
|
||||||
|
if (authResult instanceof NextResponse) return authResult;
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo || !authInfo.username) {
|
if (!authInfo || !authInfo.username) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||||
|
|||||||
+33
-10
@@ -1,11 +1,14 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import type { Metadata, Viewport } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { parseAuthInfo } from '@/lib/auth';
|
||||||
|
import { getUserFeatureAccess } from '@/lib/permissions';
|
||||||
import { listEnabledSourceScripts } from '@/lib/source-script';
|
import { listEnabledSourceScripts } from '@/lib/source-script';
|
||||||
|
|
||||||
import { StartupCacheCleanup } from '../components/DanmakuCacheCleanup';
|
import { StartupCacheCleanup } from '../components/DanmakuCacheCleanup';
|
||||||
@@ -92,18 +95,27 @@ export default async function RootLayout({
|
|||||||
let aiDefaultMessageNoVideo = '';
|
let aiDefaultMessageNoVideo = '';
|
||||||
let aiDefaultMessageWithVideo = '';
|
let aiDefaultMessageWithVideo = '';
|
||||||
let enableMovieRequest = true;
|
let enableMovieRequest = true;
|
||||||
|
let liveEnabled = true;
|
||||||
let webLiveEnabled = false;
|
let webLiveEnabled = false;
|
||||||
let customAdFilterVersion = 0;
|
let customAdFilterVersion = 0;
|
||||||
let tuneHubEnabled = false;
|
let musicFeatureEnabled = false;
|
||||||
let suwayomiEnabled = false;
|
let suwayomiEnabled = false;
|
||||||
let musicProxyEnabled = true;
|
let musicProxyEnabled = true;
|
||||||
let advancedRecommendationEnabled = false;
|
let advancedRecommendationEnabled = false;
|
||||||
|
let userFeatureAccess =
|
||||||
|
storageType === 'localstorage'
|
||||||
|
? await getUserFeatureAccess(process.env.USERNAME || 'localstorage-owner')
|
||||||
|
: await getUserFeatureAccess(null);
|
||||||
let customCategories = [] as {
|
let customCategories = [] as {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
query: string;
|
query: string;
|
||||||
}[];
|
}[];
|
||||||
if (storageType !== 'localstorage') {
|
if (storageType !== 'localstorage') {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const authInfo = parseAuthInfo(cookieStore.get('auth')?.value);
|
||||||
|
userFeatureAccess = await getUserFeatureAccess(authInfo?.username);
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
siteName = config.SiteConfig.SiteName;
|
siteName = config.SiteConfig.SiteName;
|
||||||
announcement = config.SiteConfig.Announcement;
|
announcement = config.SiteConfig.Announcement;
|
||||||
@@ -149,11 +161,12 @@ export default async function RootLayout({
|
|||||||
// 求片功能配置
|
// 求片功能配置
|
||||||
enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true;
|
enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true;
|
||||||
// 网络直播功能配置
|
// 网络直播功能配置
|
||||||
|
liveEnabled = (config.LiveConfig || []).some((source) => !source.disabled);
|
||||||
webLiveEnabled = config.WebLiveEnabled ?? false;
|
webLiveEnabled = config.WebLiveEnabled ?? false;
|
||||||
// 自定义去广告代码版本号
|
// 自定义去广告代码版本号
|
||||||
customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0;
|
customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0;
|
||||||
// 音乐功能配置
|
// 音乐功能配置
|
||||||
tuneHubEnabled = config.MusicConfig?.Enabled || false;
|
musicFeatureEnabled = config.MusicConfig?.Enabled || false;
|
||||||
musicProxyEnabled = config.MusicConfig?.ProxyEnabled ?? true;
|
musicProxyEnabled = config.MusicConfig?.ProxyEnabled ?? true;
|
||||||
// 漫画功能配置
|
// 漫画功能配置
|
||||||
suwayomiEnabled = !!(
|
suwayomiEnabled = !!(
|
||||||
@@ -204,10 +217,13 @@ export default async function RootLayout({
|
|||||||
ENABLE_TVBOX_SUBSCRIBE: process.env.ENABLE_TVBOX_SUBSCRIBE === 'true',
|
ENABLE_TVBOX_SUBSCRIBE: process.env.ENABLE_TVBOX_SUBSCRIBE === 'true',
|
||||||
ENABLE_OFFLINE_DOWNLOAD: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
ENABLE_OFFLINE_DOWNLOAD: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
||||||
VOICE_CHAT_STRATEGY: process.env.NEXT_PUBLIC_VOICE_CHAT_STRATEGY || 'webrtc-fallback',
|
VOICE_CHAT_STRATEGY: process.env.NEXT_PUBLIC_VOICE_CHAT_STRATEGY || 'webrtc-fallback',
|
||||||
OPENLIST_ENABLED: openListEnabled,
|
OPENLIST_ENABLED: openListEnabled && userFeatureAccess.private_library,
|
||||||
EMBY_ENABLED: embyEnabled,
|
EMBY_ENABLED: embyEnabled && userFeatureAccess.emby,
|
||||||
XIAOYA_ENABLED: xiaoyaEnabled,
|
XIAOYA_ENABLED: xiaoyaEnabled && userFeatureAccess.xiaoya,
|
||||||
PRIVATE_LIBRARY_ENABLED: openListEnabled || embyEnabled || xiaoyaEnabled,
|
PRIVATE_LIBRARY_ENABLED:
|
||||||
|
(openListEnabled && userFeatureAccess.private_library) ||
|
||||||
|
(embyEnabled && userFeatureAccess.emby) ||
|
||||||
|
(xiaoyaEnabled && userFeatureAccess.xiaoya),
|
||||||
LOGIN_BACKGROUND_IMAGE: loginBackgroundImage,
|
LOGIN_BACKGROUND_IMAGE: loginBackgroundImage,
|
||||||
REGISTER_BACKGROUND_IMAGE: registerBackgroundImage,
|
REGISTER_BACKGROUND_IMAGE: registerBackgroundImage,
|
||||||
PROGRESS_THUMB_TYPE: progressThumbType,
|
PROGRESS_THUMB_TYPE: progressThumbType,
|
||||||
@@ -221,7 +237,7 @@ export default async function RootLayout({
|
|||||||
ENABLE_OIDC_LOGIN: enableOIDCLogin,
|
ENABLE_OIDC_LOGIN: enableOIDCLogin,
|
||||||
ENABLE_OIDC_REGISTRATION: enableOIDCRegistration,
|
ENABLE_OIDC_REGISTRATION: enableOIDCRegistration,
|
||||||
OIDC_BUTTON_TEXT: oidcButtonText,
|
OIDC_BUTTON_TEXT: oidcButtonText,
|
||||||
AI_ENABLED: aiEnabled,
|
AI_ENABLED: aiEnabled && userFeatureAccess.ai_ask,
|
||||||
AI_ENABLE_HOMEPAGE_ENTRY: aiEnableHomepageEntry,
|
AI_ENABLE_HOMEPAGE_ENTRY: aiEnableHomepageEntry,
|
||||||
AI_ENABLE_VIDEOCARD_ENTRY: aiEnableVideoCardEntry,
|
AI_ENABLE_VIDEOCARD_ENTRY: aiEnableVideoCardEntry,
|
||||||
AI_ENABLE_PLAYPAGE_ENTRY: aiEnablePlayPageEntry,
|
AI_ENABLE_PLAYPAGE_ENTRY: aiEnablePlayPageEntry,
|
||||||
@@ -231,12 +247,19 @@ export default async function RootLayout({
|
|||||||
AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo,
|
AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo,
|
||||||
AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo,
|
AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo,
|
||||||
ENABLE_MOVIE_REQUEST: enableMovieRequest,
|
ENABLE_MOVIE_REQUEST: enableMovieRequest,
|
||||||
WEB_LIVE_ENABLED: webLiveEnabled,
|
LIVE_ENABLED: liveEnabled && userFeatureAccess.live,
|
||||||
|
WEB_LIVE_ENABLED: webLiveEnabled && userFeatureAccess.web_live,
|
||||||
ADVANCED_RECOMMENDATION_ENABLED: advancedRecommendationEnabled,
|
ADVANCED_RECOMMENDATION_ENABLED: advancedRecommendationEnabled,
|
||||||
CUSTOM_AD_FILTER_VERSION: customAdFilterVersion,
|
CUSTOM_AD_FILTER_VERSION: customAdFilterVersion,
|
||||||
MUSIC_ENABLED: tuneHubEnabled,
|
MUSIC_ENABLED: musicFeatureEnabled && userFeatureAccess.music,
|
||||||
MUSIC_PROXY_ENABLED: musicProxyEnabled,
|
MUSIC_PROXY_ENABLED: musicProxyEnabled,
|
||||||
SUWAYOMI_ENABLED: suwayomiEnabled,
|
SUWAYOMI_ENABLED: suwayomiEnabled && userFeatureAccess.manga,
|
||||||
|
NETDISK_SEARCH_ENABLED: userFeatureAccess.netdisk_search,
|
||||||
|
MAGNET_SEARCH_ENABLED: userFeatureAccess.magnet_search,
|
||||||
|
MAGNET_SAVE_PRIVATE_LIBRARY_ENABLED:
|
||||||
|
userFeatureAccess.magnet_save_private_library,
|
||||||
|
NETDISK_TRANSFER_ENABLED: userFeatureAccess.netdisk_transfer,
|
||||||
|
NETDISK_TEMP_PLAY_ENABLED: userFeatureAccess.netdisk_temp_play,
|
||||||
FESTIVE_EFFECT_ENABLED:
|
FESTIVE_EFFECT_ENABLED:
|
||||||
process.env.FESTIVE_EFFECT_ENABLED === 'true',
|
process.env.FESTIVE_EFFECT_ENABLED === 'true',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,14 +72,22 @@ interface LiveSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LivePageClient() {
|
function LivePageClient() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
// 动态加载浏览器专用库
|
// 动态加载浏览器专用库
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
import('artplayer').then(mod => { Artplayer = mod.default; });
|
import('artplayer').then(mod => { Artplayer = mod.default; });
|
||||||
import('hls.js').then(mod => { Hls = mod.default; });
|
import('hls.js').then(mod => { Hls = mod.default; });
|
||||||
import('flv.js').then(mod => { flvjs = mod.default; });
|
import('flv.js').then(mod => { flvjs = mod.default; });
|
||||||
|
|
||||||
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
|
if (runtimeConfig?.LIVE_ENABLED === false) {
|
||||||
|
router.replace('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, [router]);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// 状态变量(State)
|
// 状态变量(State)
|
||||||
@@ -91,9 +99,6 @@ function LivePageClient() {
|
|||||||
const [loadingMessage, setLoadingMessage] = useState('正在加载直播源...');
|
const [loadingMessage, setLoadingMessage] = useState('正在加载直播源...');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 直播源相关
|
// 直播源相关
|
||||||
const [liveSources, setLiveSources] = useState<LiveSource[]>([]);
|
const [liveSources, setLiveSources] = useState<LiveSource[]>([]);
|
||||||
const [currentSource, setCurrentSource] = useState<LiveSource | null>(null);
|
const [currentSource, setCurrentSource] = useState<LiveSource | null>(null);
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export default function MangaRecommendPage() {
|
|||||||
const [shelf, setShelf] = useState<Record<string, MangaShelfItem>>({});
|
const [shelf, setShelf] = useState<Record<string, MangaShelfItem>>({});
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined' && !(window as any).RUNTIME_CONFIG?.SUWAYOMI_ENABLED) {
|
||||||
|
router.replace('/');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const query = searchParams.get('q')?.trim();
|
const query = searchParams.get('q')?.trim();
|
||||||
if (!query) return;
|
if (!query) return;
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ export default function MusicPage() {
|
|||||||
const [loadingPlayAll, setLoadingPlayAll] = useState(false); // 播放全部加载状态
|
const [loadingPlayAll, setLoadingPlayAll] = useState(false); // 播放全部加载状态
|
||||||
const [deletingPlaylistId, setDeletingPlaylistId] = useState<string | null>(null); // 正在删除的歌单ID
|
const [deletingPlaylistId, setDeletingPlaylistId] = useState<string | null>(null); // 正在删除的歌单ID
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined' && !(window as any).RUNTIME_CONFIG?.MUSIC_ENABLED) {
|
||||||
|
router.replace('/');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
// Toast 和 Confirm Modal 状态
|
// Toast 和 Confirm Modal 状态
|
||||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||||
const [confirmModal, setConfirmModal] = useState<{
|
const [confirmModal, setConfirmModal] = useState<{
|
||||||
|
|||||||
+21
-10
@@ -148,6 +148,7 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
// 网盘搜索弹窗状态
|
// 网盘搜索弹窗状态
|
||||||
const [showPansouDialog, setShowPansouDialog] = useState(false);
|
const [showPansouDialog, setShowPansouDialog] = useState(false);
|
||||||
|
const [netdiskSearchEnabled, setNetdiskSearchEnabled] = useState(false);
|
||||||
|
|
||||||
// AI问片状态
|
// AI问片状态
|
||||||
const [showAIChat, setShowAIChat] = useState(false);
|
const [showAIChat, setShowAIChat] = useState(false);
|
||||||
@@ -218,6 +219,14 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setNetdiskSearchEnabled(
|
||||||
|
!!(window as any).RUNTIME_CONFIG?.NETDISK_SEARCH_ENABLED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 网页全屏状态 - 控制导航栏的显示隐藏
|
// 网页全屏状态 - 控制导航栏的显示隐藏
|
||||||
const [isWebFullscreen, setIsWebFullscreen] = useState(false);
|
const [isWebFullscreen, setIsWebFullscreen] = useState(false);
|
||||||
// 原生全屏状态
|
// 原生全屏状态
|
||||||
@@ -9503,16 +9512,18 @@ function PlayPageClient() {
|
|||||||
<FavoriteIcon filled={favorited} />
|
<FavoriteIcon filled={favorited} />
|
||||||
</button>
|
</button>
|
||||||
{/* 网盘搜索按钮 */}
|
{/* 网盘搜索按钮 */}
|
||||||
<button
|
{netdiskSearchEnabled && (
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
openDrawer('pansou');
|
e.stopPropagation();
|
||||||
}}
|
openDrawer('pansou');
|
||||||
className='flex-shrink-0 hover:opacity-80 transition-opacity'
|
}}
|
||||||
title='搜索网盘资源'
|
className='flex-shrink-0 hover:opacity-80 transition-opacity'
|
||||||
>
|
title='搜索网盘资源'
|
||||||
<Cloud className='h-6 w-6 text-gray-700 dark:text-gray-300' />
|
>
|
||||||
</button>
|
<Cloud className='h-6 w-6 text-gray-700 dark:text-gray-300' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* AI问片按钮 */}
|
{/* AI问片按钮 */}
|
||||||
{aiEnabled && detail && (
|
{aiEnabled && detail && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ export default function PrivateLibraryPage() {
|
|||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted && !runtimeConfig.PRIVATE_LIBRARY_ENABLED) {
|
||||||
|
router.replace('/');
|
||||||
|
}
|
||||||
|
}, [mounted, router, runtimeConfig]);
|
||||||
|
|
||||||
// 小雅搜索处理函数
|
// 小雅搜索处理函数
|
||||||
const handleXiaoyaSearch = async () => {
|
const handleXiaoyaSearch = async () => {
|
||||||
if (!xiaoyaSearchKeyword.trim()) return;
|
if (!xiaoyaSearchKeyword.trim()) return;
|
||||||
|
|||||||
+23
-9
@@ -63,6 +63,8 @@ function SearchPageClient() {
|
|||||||
const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>(
|
const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [netdiskSearchEnabled, setNetdiskSearchEnabled] = useState(false);
|
||||||
|
const [magnetSearchEnabled, setMagnetSearchEnabled] = useState(false);
|
||||||
// 繁体转简体转换器
|
// 繁体转简体转换器
|
||||||
const converterRef = useRef<((text: string) => string) | null>(null);
|
const converterRef = useRef<((text: string) => string) | null>(null);
|
||||||
// 转换器是否已初始化
|
// 转换器是否已初始化
|
||||||
@@ -964,7 +966,10 @@ function SearchPageClient() {
|
|||||||
const typeParam = searchParams.get('type');
|
const typeParam = searchParams.get('type');
|
||||||
const query = searchParams.get('q');
|
const query = searchParams.get('q');
|
||||||
|
|
||||||
if (typeParam === 'pansou' || typeParam === 'acg') {
|
if (
|
||||||
|
(typeParam === 'pansou' && netdiskSearchEnabled) ||
|
||||||
|
(typeParam === 'acg' && magnetSearchEnabled)
|
||||||
|
) {
|
||||||
setActiveTab(typeParam);
|
setActiveTab(typeParam);
|
||||||
|
|
||||||
// 如果有搜索关键词且显示结果,触发对应的搜索
|
// 如果有搜索关键词且显示结果,触发对应的搜索
|
||||||
@@ -994,6 +999,12 @@ function SearchPageClient() {
|
|||||||
// 获取用户权限
|
// 获取用户权限
|
||||||
const authInfo = getAuthInfoFromBrowserCookie();
|
const authInfo = getAuthInfoFromBrowserCookie();
|
||||||
setUserRole(authInfo?.role || null);
|
setUserRole(authInfo?.role || null);
|
||||||
|
setNetdiskSearchEnabled(
|
||||||
|
!!(window as any).RUNTIME_CONFIG?.NETDISK_SEARCH_ENABLED
|
||||||
|
);
|
||||||
|
setMagnetSearchEnabled(
|
||||||
|
!!(window as any).RUNTIME_CONFIG?.MAGNET_SEARCH_ENABLED
|
||||||
|
);
|
||||||
|
|
||||||
// 初始化繁体转简体转换器
|
// 初始化繁体转简体转换器
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -1337,7 +1348,7 @@ function SearchPageClient() {
|
|||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
}
|
}
|
||||||
}, [searchParams, forceRefresh, converterReady]);
|
}, [searchParams, forceRefresh, converterReady, netdiskSearchEnabled, magnetSearchEnabled]);
|
||||||
|
|
||||||
// 组件卸载时,关闭可能存在的连接
|
// 组件卸载时,关闭可能存在的连接
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1558,13 +1569,16 @@ function SearchPageClient() {
|
|||||||
value: 'video',
|
value: 'video',
|
||||||
icon: <Film size={16} />,
|
icon: <Film size={16} />,
|
||||||
},
|
},
|
||||||
{
|
...(netdiskSearchEnabled
|
||||||
label: '网盘搜索',
|
? [
|
||||||
value: 'pansou',
|
{
|
||||||
icon: <HardDrive size={16} />,
|
label: '网盘搜索',
|
||||||
},
|
value: 'pansou' as const,
|
||||||
// 仅管理员和站长显示 ACG 磁力搜索
|
icon: <HardDrive size={16} />,
|
||||||
...(userRole === 'admin' || userRole === 'owner'
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(magnetSearchEnabled
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: '动漫磁力',
|
label: '动漫磁力',
|
||||||
|
|||||||
@@ -54,16 +54,11 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
label: '综艺',
|
label: '综艺',
|
||||||
href: '/douban?type=show',
|
href: '/douban?type=show',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: TvMinimalPlay,
|
icon: TvMinimalPlay,
|
||||||
label: '电视直播',
|
label: '电视直播',
|
||||||
href: '/live',
|
href: '/live',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: Globe,
|
|
||||||
label: '网络直播',
|
|
||||||
href: '/web-live',
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,11 +87,15 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
label: '综艺',
|
label: '综艺',
|
||||||
href: '/douban?type=show',
|
href: '/douban?type=show',
|
||||||
},
|
},
|
||||||
{
|
...(runtimeConfig?.LIVE_ENABLED
|
||||||
icon: TvMinimalPlay,
|
? [
|
||||||
label: '电视直播',
|
{
|
||||||
href: '/live',
|
icon: TvMinimalPlay,
|
||||||
},
|
label: '电视直播',
|
||||||
|
href: '/live',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 如果启用网络直播,添加网络直播入口
|
// 如果启用网络直播,添加网络直播入口
|
||||||
|
|||||||
@@ -151,11 +151,6 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
label: '电视直播',
|
label: '电视直播',
|
||||||
href: '/live',
|
href: '/live',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: Globe,
|
|
||||||
label: '网络直播',
|
|
||||||
href: '/web-live',
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -183,11 +178,15 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
label: '综艺',
|
label: '综艺',
|
||||||
href: '/douban?type=show',
|
href: '/douban?type=show',
|
||||||
},
|
},
|
||||||
{
|
...(runtimeConfig?.LIVE_ENABLED
|
||||||
icon: TvMinimalPlay,
|
? [
|
||||||
label: '电视直播',
|
{
|
||||||
href: '/live',
|
icon: TvMinimalPlay,
|
||||||
},
|
label: '电视直播',
|
||||||
|
href: '/live',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 如果启用网络直播,添加网络直播入口
|
// 如果启用网络直播,添加网络直播入口
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export interface AdminConfig {
|
|||||||
Tags?: {
|
Tags?: {
|
||||||
name: string;
|
name: string;
|
||||||
enabledApis: string[];
|
enabledApis: string[];
|
||||||
|
permissions?: string[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
SourceConfig: {
|
SourceConfig: {
|
||||||
@@ -189,8 +190,6 @@ export interface AdminConfig {
|
|||||||
EnableVideoCardEntry: boolean; // VideoCard入口开关
|
EnableVideoCardEntry: boolean; // VideoCard入口开关
|
||||||
EnablePlayPageEntry: boolean; // 播放页入口开关
|
EnablePlayPageEntry: boolean; // 播放页入口开关
|
||||||
EnableAIComments: boolean; // AI评论生成开关
|
EnableAIComments: boolean; // AI评论生成开关
|
||||||
// 权限控制
|
|
||||||
AllowRegularUsers: boolean; // 是否允许普通用户使用AI问片(关闭后仅站长和管理员可用)
|
|
||||||
// 高级设置
|
// 高级设置
|
||||||
Temperature?: number; // AI温度参数(0-2),默认0.7
|
Temperature?: number; // AI温度参数(0-2),默认0.7
|
||||||
MaxTokens?: number; // 最大回复token数,默认1000
|
MaxTokens?: number; // 最大回复token数,默认1000
|
||||||
@@ -276,10 +275,6 @@ export interface AdminConfig {
|
|||||||
BaseUrl?: string; // lxserver 地址
|
BaseUrl?: string; // lxserver 地址
|
||||||
Token?: string; // lxserver x-user-token
|
Token?: string; // lxserver x-user-token
|
||||||
ProxyEnabled?: boolean; // 是否走 stream 代理
|
ProxyEnabled?: boolean; // 是否走 stream 代理
|
||||||
// 兼容旧代码的遗留字段(待删除)
|
|
||||||
TuneHubEnabled?: boolean;
|
|
||||||
TuneHubBaseUrl?: string;
|
|
||||||
TuneHubApiKey?: string;
|
|
||||||
OpenListCacheEnabled?: boolean;
|
OpenListCacheEnabled?: boolean;
|
||||||
OpenListCacheURL?: string;
|
OpenListCacheURL?: string;
|
||||||
OpenListCacheUsername?: string;
|
OpenListCacheUsername?: string;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export const FEATURE_PERMISSION_OPTIONS = [
|
||||||
|
{ key: 'private_library', label: '私人影库', description: 'OpenList 私人影库访问' },
|
||||||
|
{ key: 'emby', label: 'Emby', description: 'Emby 私人媒体库访问' },
|
||||||
|
{ key: 'xiaoya', label: '小雅', description: '小雅媒体库访问' },
|
||||||
|
{ key: 'ai_ask', label: 'AI问片', description: 'AI 问片与影视问答' },
|
||||||
|
{ key: 'netdisk_search', label: '网盘搜索', description: 'Pansou 网盘资源搜索' },
|
||||||
|
{ key: 'magnet_search', label: '磁链搜索', description: '动漫/磁链搜索' },
|
||||||
|
{ key: 'magnet_save_private_library', label: '磁链保存影库', description: '磁链保存到私人影库' },
|
||||||
|
{ key: 'netdisk_transfer', label: '网盘转存', description: '夸克网盘转存' },
|
||||||
|
{ key: 'netdisk_temp_play', label: '临时播放', description: '网盘资源临时播放' },
|
||||||
|
{ key: 'live', label: '电视直播', description: '电视直播频道观看' },
|
||||||
|
{ key: 'web_live', label: '网络直播', description: '网络直播观看' },
|
||||||
|
{ key: 'music', label: '音乐', description: '音乐视听功能' },
|
||||||
|
{ key: 'manga', label: '漫画展馆', description: '漫画搜索、阅读与书架' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type FeaturePermissionKey = (typeof FEATURE_PERMISSION_OPTIONS)[number]['key'];
|
||||||
|
|
||||||
|
export const ALL_FEATURE_PERMISSION_KEYS = FEATURE_PERMISSION_OPTIONS.map(
|
||||||
|
(item) => item.key
|
||||||
|
) as FeaturePermissionKey[];
|
||||||
|
|
||||||
|
export function sanitizeFeaturePermissions(
|
||||||
|
permissions?: string[] | null
|
||||||
|
): FeaturePermissionKey[] {
|
||||||
|
if (!Array.isArray(permissions)) return [];
|
||||||
|
const allowed = new Set<FeaturePermissionKey>(ALL_FEATURE_PERMISSION_KEYS);
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
permissions.filter(
|
||||||
|
(item): item is FeaturePermissionKey =>
|
||||||
|
typeof item === 'string' && allowed.has(item as FeaturePermissionKey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { hasFeaturePermission } from '@/lib/permissions';
|
||||||
|
|
||||||
export async function getMusicV2Username(request: NextRequest): Promise<string | null> {
|
export async function getMusicV2Username(request: NextRequest): Promise<string | null> {
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
@@ -14,6 +15,11 @@ export async function getMusicV2Username(request: NextRequest): Promise<string |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowed = await hasFeaturePermission(authInfo.username, 'music');
|
||||||
|
if (!allowed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return authInfo.username;
|
return authInfo.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import {
|
||||||
|
ALL_FEATURE_PERMISSION_KEYS,
|
||||||
|
type FeaturePermissionKey,
|
||||||
|
sanitizeFeaturePermissions,
|
||||||
|
} from '@/lib/feature-permissions';
|
||||||
|
|
||||||
|
export type FeatureAccessMap = Record<FeaturePermissionKey, boolean>;
|
||||||
|
|
||||||
|
export function createEmptyFeatureAccessMap(): FeatureAccessMap {
|
||||||
|
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = false;
|
||||||
|
return acc;
|
||||||
|
}, {} as FeatureAccessMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivilegedRole(role?: string) {
|
||||||
|
return role === 'owner' || role === 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserFeatureAccessMap(username: string): Promise<FeatureAccessMap> {
|
||||||
|
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||||
|
if (storageType === 'localstorage') {
|
||||||
|
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as FeatureAccessMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
|
if (!userInfo || userInfo.banned) {
|
||||||
|
return createEmptyFeatureAccessMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username === process.env.USERNAME || isPrivilegedRole(userInfo.role)) {
|
||||||
|
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as FeatureAccessMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getConfig();
|
||||||
|
const tags = Array.isArray(userInfo.tags) ? userInfo.tags : [];
|
||||||
|
|
||||||
|
// 兼容旧用户:未分配用户组时,默认拥有全部功能权限
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as FeatureAccessMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedPermissions = new Set<FeaturePermissionKey>();
|
||||||
|
|
||||||
|
tags.forEach((tagName) => {
|
||||||
|
const group = config.UserConfig.Tags?.find((item) => item.name === tagName);
|
||||||
|
sanitizeFeaturePermissions(group?.permissions).forEach((permission) =>
|
||||||
|
allowedPermissions.add(permission)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
|
||||||
|
acc[key] = allowedPermissions.has(key);
|
||||||
|
return acc;
|
||||||
|
}, {} as FeatureAccessMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserFeatureAccess(username?: string | null): Promise<FeatureAccessMap> {
|
||||||
|
if (!username) return createEmptyFeatureAccessMap();
|
||||||
|
return getUserFeatureAccessMap(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasFeaturePermission(
|
||||||
|
username: string,
|
||||||
|
permission: FeaturePermissionKey
|
||||||
|
): Promise<boolean> {
|
||||||
|
const accessMap = await getUserFeatureAccessMap(username);
|
||||||
|
return accessMap[permission] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireFeaturePermission(
|
||||||
|
request: NextRequest,
|
||||||
|
permission: FeaturePermissionKey,
|
||||||
|
errorMessage = '无权限访问该功能'
|
||||||
|
): Promise<{ username: string } | NextResponse> {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo?.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = await hasFeaturePermission(authInfo.username, permission);
|
||||||
|
if (!allowed) {
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { username: authInfo.username };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user