feat: limit config file edit to owner
This commit is contained in:
+35
-12
@@ -1282,7 +1282,7 @@ const CategoryConfig = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 新增配置文件组件
|
// 新增配置文件组件
|
||||||
const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise<void> }) => {
|
const ConfigFileComponent = ({ config, refreshConfig, role }: { config: AdminConfig | null; refreshConfig: () => Promise<void>; role: 'owner' | 'admin' | null }) => {
|
||||||
const [configContent, setConfigContent] = useState('');
|
const [configContent, setConfigContent] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [subscriptionUrl, setSubscriptionUrl] = useState('');
|
const [subscriptionUrl, setSubscriptionUrl] = useState('');
|
||||||
@@ -1290,6 +1290,9 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
const [lastCheckTime, setLastCheckTime] = useState<string>('');
|
const [lastCheckTime, setLastCheckTime] = useState<string>('');
|
||||||
|
|
||||||
|
// 检查是否为站长
|
||||||
|
const isOwner = role === 'owner';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.ConfigFile) {
|
if (config?.ConfigFile) {
|
||||||
setConfigContent(config.ConfigFile);
|
setConfigContent(config.ConfigFile);
|
||||||
@@ -1381,8 +1384,22 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
|
{/* 非站长用户权限提示 */}
|
||||||
|
{!isOwner && (
|
||||||
|
<div className='bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center'>
|
||||||
|
<span className='text-white text-xs font-bold'>!</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-amber-800 dark:text-amber-300 text-sm font-medium'>
|
||||||
|
配置文件模块仅站长可编辑,您只能查看配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 配置订阅区域 */}
|
{/* 配置订阅区域 */}
|
||||||
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 shadow-sm'>
|
<div className={`bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 shadow-sm ${!isOwner ? 'opacity-60' : ''}`}>
|
||||||
<div className='flex items-center justify-between mb-6'>
|
<div className='flex items-center justify-between mb-6'>
|
||||||
<h3 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
|
<h3 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
配置订阅
|
配置订阅
|
||||||
@@ -1403,7 +1420,8 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
value={subscriptionUrl}
|
value={subscriptionUrl}
|
||||||
onChange={(e) => setSubscriptionUrl(e.target.value)}
|
onChange={(e) => setSubscriptionUrl(e.target.value)}
|
||||||
placeholder='https://example.com/config.json'
|
placeholder='https://example.com/config.json'
|
||||||
className='w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
|
disabled={!isOwner}
|
||||||
|
className={`w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${!isOwner ? 'cursor-not-allowed bg-gray-100 dark:bg-gray-700' : ''}`}
|
||||||
/>
|
/>
|
||||||
<p className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
|
<p className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
输入配置文件的订阅地址,要求 JSON 格式,且使用 Base58 编码
|
输入配置文件的订阅地址,要求 JSON 格式,且使用 Base58 编码
|
||||||
@@ -1414,8 +1432,8 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
<div className='pt-2'>
|
<div className='pt-2'>
|
||||||
<button
|
<button
|
||||||
onClick={handleFetchConfig}
|
onClick={handleFetchConfig}
|
||||||
disabled={fetching || !subscriptionUrl.trim()}
|
disabled={!isOwner || fetching || !subscriptionUrl.trim()}
|
||||||
className={`w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${fetching || !subscriptionUrl.trim()
|
className={`w-full px-6 py-3 rounded-lg font-medium transition-all duration-200 ${!isOwner || fetching || !subscriptionUrl.trim()
|
||||||
? 'bg-gray-300 dark:bg-gray-600 cursor-not-allowed text-gray-500 dark:text-gray-400'
|
? 'bg-gray-300 dark:bg-gray-600 cursor-not-allowed text-gray-500 dark:text-gray-400'
|
||||||
: 'bg-green-600 hover:bg-green-700 text-white shadow-sm hover:shadow-md transform hover:-translate-y-0.5'
|
: 'bg-green-600 hover:bg-green-700 text-white shadow-sm hover:shadow-md transform hover:-translate-y-0.5'
|
||||||
}`}
|
}`}
|
||||||
@@ -1444,9 +1462,13 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={() => setAutoUpdate(!autoUpdate)}
|
onClick={() => setAutoUpdate(!autoUpdate)}
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${autoUpdate
|
disabled={!isOwner}
|
||||||
? 'bg-green-600'
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${!isOwner
|
||||||
: 'bg-gray-200 dark:bg-gray-700'
|
? 'cursor-not-allowed opacity-50'
|
||||||
|
: ''
|
||||||
|
} ${autoUpdate
|
||||||
|
? 'bg-green-600'
|
||||||
|
: 'bg-gray-200 dark:bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1468,7 +1490,8 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
onChange={(e) => setConfigContent(e.target.value)}
|
onChange={(e) => setConfigContent(e.target.value)}
|
||||||
rows={20}
|
rows={20}
|
||||||
placeholder='请输入配置文件内容(JSON 格式)...'
|
placeholder='请输入配置文件内容(JSON 格式)...'
|
||||||
className='w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm leading-relaxed resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500'
|
disabled={!isOwner}
|
||||||
|
className={`w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm leading-relaxed resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500 ${!isOwner ? 'cursor-not-allowed bg-gray-100 dark:bg-gray-700' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
|
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
|
||||||
}}
|
}}
|
||||||
@@ -1483,8 +1506,8 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig |
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={!isOwner || saving}
|
||||||
className={`px-4 py-2 rounded-lg transition-colors ${saving
|
className={`px-4 py-2 rounded-lg transition-colors ${!isOwner || saving
|
||||||
? 'bg-gray-400 cursor-not-allowed text-white'
|
? 'bg-gray-400 cursor-not-allowed text-white'
|
||||||
: 'bg-green-600 hover:bg-green-700 text-white'
|
: 'bg-green-600 hover:bg-green-700 text-white'
|
||||||
}`}
|
}`}
|
||||||
@@ -2145,7 +2168,7 @@ function AdminPageClient() {
|
|||||||
isExpanded={expandedTabs.configFile}
|
isExpanded={expandedTabs.configFile}
|
||||||
onToggle={() => toggleTab('configFile')}
|
onToggle={() => toggleTab('configFile')}
|
||||||
>
|
>
|
||||||
<ConfigFileComponent config={config} refreshConfig={fetchConfig} />
|
<ConfigFileComponent config={config} refreshConfig={fetchConfig} role={role} />
|
||||||
</CollapsibleTab>
|
</CollapsibleTab>
|
||||||
|
|
||||||
{/* 站点配置标签 */}
|
{/* 站点配置标签 */}
|
||||||
|
|||||||
@@ -28,14 +28,12 @@ export async function POST(request: NextRequest) {
|
|||||||
let adminConfig = await getConfig();
|
let adminConfig = await getConfig();
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
|
|
||||||
|
// 仅站长可以修改配置文件
|
||||||
if (username !== process.env.USERNAME) {
|
if (username !== process.env.USERNAME) {
|
||||||
const user = adminConfig.UserConfig.Users.find((u) => u.username === username);
|
return NextResponse.json(
|
||||||
if (!user || user.role !== 'admin' || user.banned) {
|
{ error: '权限不足,只有站长可以修改配置文件' },
|
||||||
return NextResponse.json(
|
{ status: 403 }
|
||||||
{ error: '权限不足,只有管理员可以修改配置文件' },
|
);
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取请求体
|
// 获取请求体
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
// 权限检查:仅站长可以拉取配置订阅
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '权限不足,只有站长可以拉取配置订阅' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { url } = await request.json();
|
const { url } = await request.json();
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -27,7 +44,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const decodedBytes = bs58.decode(configContent);
|
const decodedBytes = bs58.decode(configContent);
|
||||||
decodedContent = new TextDecoder().decode(decodedBytes);
|
decodedContent = new TextDecoder().decode(decodedBytes);
|
||||||
} catch (decodeError) {
|
} catch (decodeError) {
|
||||||
console.warn('Base58 解码失败,返回原始内容:', decodeError);
|
console.warn('Base58 解码失败', decodeError);
|
||||||
throw decodeError;
|
throw decodeError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getConfig, refineConfig } from '@/lib/config';
|
||||||
import { db, getStorage } from '@/lib/db';
|
import { db, getStorage } from '@/lib/db';
|
||||||
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||||
import { SearchResult } from '@/lib/types';
|
import { SearchResult } from '@/lib/types';
|
||||||
import { getConfig, refineConfig } from '@/lib/config';
|
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAvailableApiSites } from '@/lib/config';
|
import { getAvailableApiSites } from '@/lib/config';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { CURRENT_VERSION } from '@/lib/version'
|
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { CURRENT_VERSION } from '@/lib/version'
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import { SearchResult } from '@/lib/types';
|
|||||||
import { yellowWords } from '@/lib/yellow';
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
|
||||||
import SearchSuggestions from '@/components/SearchSuggestions';
|
import SearchSuggestions from '@/components/SearchSuggestions';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
|
|
||||||
|
|
||||||
function SearchPageClient() {
|
function SearchPageClient() {
|
||||||
// 搜索历史
|
// 搜索历史
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowDownWideNarrow, ArrowUpNarrowWide } from 'lucide-react';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { ArrowDownWideNarrow, ArrowUpNarrowWide } from 'lucide-react';
|
|
||||||
|
|
||||||
export type SearchFilterKey = 'source' | 'title' | 'year' | 'yearOrder';
|
export type SearchFilterKey = 'source' | 'title' | 'year' | 'yearOrder';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
import { Heart, Link, PlayCircleIcon, Trash2 } from 'lucide-react';
|
import { Heart, Link, PlayCircleIcon, Trash2 } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|||||||
Reference in New Issue
Block a user