From 5fcdcbb390dd7ad16e65817de28e52d7c413468f Mon Sep 17 00:00:00 2001 From: shinya Date: Sat, 5 Jul 2025 18:49:35 +0800 Subject: [PATCH] feat: implement all admin functions --- Dockerfile | 6 +- package.json | 3 +- pnpm-lock.yaml | 8 + src/app/admin/page.tsx | 600 ++++++++++++++++++++++-------- src/app/api/admin/config/route.ts | 55 ++- src/app/api/admin/reset/route.ts | 55 +++ src/app/api/admin/site/route.ts | 123 ++++++ src/app/api/admin/source/route.ts | 184 +++++++++ src/app/api/admin/user/route.ts | 260 +++++++++++++ src/app/api/login/route.ts | 9 +- src/app/layout.tsx | 1 + src/lib/config.ts | 315 ++++++++++------ start.js | 28 ++ 13 files changed, 1337 insertions(+), 310 deletions(-) create mode 100644 src/app/api/admin/reset/route.ts create mode 100644 src/app/api/admin/site/route.ts create mode 100644 src/app/api/admin/source/route.ts create mode 100644 src/app/api/admin/user/route.ts create mode 100644 start.js diff --git a/Dockerfile b/Dockerfile index 097a644..6ef693e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,8 @@ ENV DOCKER_ENV=true # 从构建器中复制 standalone 输出 COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +# 从构建器中复制 start.js +COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js # 从构建器中复制 public 和 .next/static 目录 COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static @@ -57,5 +59,5 @@ USER nextjs EXPOSE 3000 -# 使用 node 直接运行 server.js -CMD ["node", "server.js"] \ No newline at end of file +# 使用自定义启动脚本,先预加载配置再启动服务器 +CMD ["node", "start.js"] \ No newline at end of file diff --git a/package.json b/package.json index 9435622..8d8a0a3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "swiper": "^11.2.8", "tailwind-merge": "^2.6.0", "vidstack": "^0.6.15", - "zod": "^3.24.1" + "zod": "^3.24.1", + "sweetalert2": "^11.11.0" }, "devDependencies": { "@commitlint/cli": "^16.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3a3560..d405333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: redis: specifier: ^4.6.7 version: 4.7.1 + sweetalert2: + specifier: ^11.11.0 + version: 11.22.2 swiper: specifier: ^11.2.8 version: 11.2.8 @@ -5725,6 +5728,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + sweetalert2@11.22.2: + resolution: {integrity: sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==} + swiper@11.2.8: resolution: {integrity: sha512-S5FVf6zWynPWooi7pJ7lZhSUe2snTzqLuUzbd5h5PHUOhzgvW0bLKBd2wv0ixn6/5o9vwc/IkQT74CRcLJQzeg==} engines: {node: '>= 4.7.0'} @@ -12608,6 +12614,8 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + sweetalert2@11.22.2: {} + swiper@11.2.8: {} symbol-tree@3.2.4: {} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index bbecc0e..a34ff52 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -23,12 +23,26 @@ import { import { CSS } from '@dnd-kit/utilities'; import { ChevronDown, ChevronUp, Settings, Users, Video } from 'lucide-react'; import { GripVertical } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import Swal from 'sweetalert2'; import { AdminConfig, AdminConfigResult } from '@/lib/admin.types'; import PageLayout from '@/components/PageLayout'; +// 统一弹窗方法(必须在首次使用前定义) +const showError = (message: string) => + Swal.fire({ icon: 'error', title: '错误', text: message }); + +const showSuccess = (message: string) => + Swal.fire({ + icon: 'success', + title: '成功', + text: message, + timer: 2000, + showConfirmButton: false, + }); + // 新增站点配置类型 interface SiteConfig { SiteName: string; @@ -87,7 +101,13 @@ const CollapsibleTab = ({ }; // 用户配置组件 -const UserConfig = ({ config }: { config: AdminConfig | null }) => { +interface UserConfigProps { + config: AdminConfig | null; + role: 'owner' | 'admin' | null; + refreshConfig: () => Promise; +} + +const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const [userSettings, setUserSettings] = useState({ enableRegistration: false, }); @@ -97,6 +117,10 @@ const UserConfig = ({ config }: { config: AdminConfig | null }) => { password: '', }); + // 当前登录用户名 + const currentUsername = + typeof window !== 'undefined' ? localStorage.getItem('username') : null; + useEffect(() => { if (config?.UserConfig) { setUserSettings({ @@ -105,33 +129,109 @@ const UserConfig = ({ config }: { config: AdminConfig | null }) => { } }, [config]); - const handleBanUser = (username: string) => { - // 这里应该调用API来封禁用户 - console.log('封禁用户:', username); + // 切换允许注册设置 + const toggleAllowRegister = async (value: boolean) => { + const username = + typeof window !== 'undefined' ? localStorage.getItem('username') : null; + const password = + typeof window !== 'undefined' ? localStorage.getItem('password') : null; + if (!username || !password) { + showError('无法获取当前用户信息,请重新登录'); + return; + } + + try { + // 先更新本地 UI + setUserSettings((prev) => ({ ...prev, enableRegistration: value })); + + const res = await fetch('/api/admin/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username, + password, + action: 'setAllowRegister', + allowRegister: value, + }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || `操作失败: ${res.status}`); + } + + await refreshConfig(); + } catch (err) { + showError(err instanceof Error ? err.message : '操作失败'); + // revert toggle UI + setUserSettings((prev) => ({ ...prev, enableRegistration: !value })); + } }; - const handleUnbanUser = (username: string) => { - // 这里应该调用API来解封用户 - console.log('解封用户:', username); + const handleBanUser = async (uname: string) => { + await handleUserAction('ban', uname); }; - const handleSetAdmin = (username: string) => { - // 这里应该调用API来设为管理员 - console.log('设为管理员:', username); + const handleUnbanUser = async (uname: string) => { + await handleUserAction('unban', uname); }; - const handleRemoveAdmin = (username: string) => { - // 这里应该调用API来取消管理员 - console.log('取消管理员:', username); + const handleSetAdmin = async (uname: string) => { + await handleUserAction('setAdmin', uname); }; - const handleAddUser = () => { - // 这里应该调用API来添加用户,默认角色为 user - console.log('添加用户:', { ...newUser, role: 'user' }); + const handleRemoveAdmin = async (uname: string) => { + await handleUserAction('cancelAdmin', uname); + }; + + const handleAddUser = async () => { + if (!newUser.username || !newUser.password) return; + await handleUserAction('add', newUser.username, newUser.password); setNewUser({ username: '', password: '' }); setShowAddUserForm(false); }; + // 通用请求函数 + const handleUserAction = async ( + action: 'add' | 'ban' | 'unban' | 'setAdmin' | 'cancelAdmin', + targetUsername: string, + targetPassword?: string + ) => { + const username = + typeof window !== 'undefined' ? localStorage.getItem('username') : null; + const password = + typeof window !== 'undefined' ? localStorage.getItem('password') : null; + + if (!username || !password) { + showError('无法获取当前用户信息,请重新登录'); + return; + } + + try { + const res = await fetch('/api/admin/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username, + password, + targetUsername, + ...(targetPassword ? { targetPassword } : {}), + action, + }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || `操作失败: ${res.status}`); + } + + // 成功后刷新配置(无需整页刷新) + await refreshConfig(); + } catch (err) { + showError(err instanceof Error ? err.message : '操作失败'); + } + }; + if (!config) { return (
@@ -168,10 +268,7 @@ const UserConfig = ({ config }: { config: AdminConfig | null }) => { - ) : user.role === 'admin' ? ( - - ) : null} - {user.role !== 'owner' && - (!user.banned ? ( - - ) : ( - - ))} - - - ))} - + + {user.username} + + + + {user.role === 'owner' + ? '站长' + : user.role === 'admin' + ? '管理员' + : '普通用户'} + + + + + {!user.banned ? '正常' : '已封禁'} + + + + {canOperate && ( + <> + {user.role === 'user' && ( + + )} + {user.role === 'admin' && ( + + )} + {user.role !== 'owner' && + (!user.banned ? ( + + ) : ( + + ))} + + )} + + + ); + })} + + ); + })()}
@@ -349,7 +476,13 @@ const UserConfig = ({ config }: { config: AdminConfig | null }) => { }; // 视频源配置组件 -const VideoSourceConfig = ({ config }: { config: AdminConfig | null }) => { +const VideoSourceConfig = ({ + config, + refreshConfig, +}: { + config: AdminConfig | null; + refreshConfig: () => Promise; +}) => { const [sources, setSources] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [orderChanged, setOrderChanged] = useState(false); @@ -381,34 +514,81 @@ const VideoSourceConfig = ({ config }: { config: AdminConfig | null }) => { useEffect(() => { if (config?.SourceConfig) { setSources(config.SourceConfig); + // 进入时重置 orderChanged + setOrderChanged(false); } }, [config]); + // 通用 API 请求 + const callSourceApi = async (body: Record) => { + const username = + typeof window !== 'undefined' ? localStorage.getItem('username') : null; + const password = + typeof window !== 'undefined' ? localStorage.getItem('password') : null; + + if (!username || !password) { + showError('无法获取当前用户信息,请重新登录'); + throw new Error('no-credential'); + } + + try { + const resp = await fetch('/api/admin/source', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password, ...body }), + }); + + if (!resp.ok) { + const data = await resp.json().catch(() => ({})); + throw new Error(data.error || `操作失败: ${resp.status}`); + } + + // 成功后刷新配置 + await refreshConfig(); + } catch (err) { + showError(err instanceof Error ? err.message : '操作失败'); + throw err; // 向上抛出方便调用处判断 + } + }; + const handleToggleEnable = (key: string) => { - setSources((prev) => - prev.map((source) => - source.key === key ? { ...source, disabled: !source.disabled } : source - ) - ); + const target = sources.find((s) => s.key === key); + if (!target) return; + const action = target.disabled ? 'enable' : 'disable'; + callSourceApi({ action, key }).catch(() => { + console.error('操作失败', action, key); + }); }; const handleDelete = (key: string) => { - setSources((prev) => prev.filter((source) => source.key !== key)); + callSourceApi({ action: 'delete', key }).catch(() => { + console.error('操作失败', 'delete', key); + }); }; const handleAddSource = () => { if (!newSource.name || !newSource.key || !newSource.api) return; - setSources((prev) => [...prev, newSource]); - setNewSource({ - name: '', - key: '', - api: '', - detail: '', - disabled: false, - from: 'custom', - }); - setShowAddForm(false); - setOrderChanged(true); + callSourceApi({ + action: 'add', + key: newSource.key, + name: newSource.name, + api: newSource.api, + detail: newSource.detail, + }) + .then(() => { + setNewSource({ + name: '', + key: '', + api: '', + detail: '', + disabled: false, + from: 'custom', + }); + setShowAddForm(false); + }) + .catch(() => { + console.error('操作失败', 'add', newSource); + }); }; const handleDragEnd = (event: any) => { @@ -421,9 +601,14 @@ const VideoSourceConfig = ({ config }: { config: AdminConfig | null }) => { }; const handleSaveOrder = () => { - console.log('保存排序:', sources); - // TODO: 调用 API 保存排序 - setOrderChanged(false); + const order = sources.map((s) => s.key); + callSourceApi({ action: 'sort', order }) + .then(() => { + setOrderChanged(false); + }) + .catch(() => { + console.error('操作失败', 'sort', order); + }); }; // 可拖拽行封装 (dnd-kit) @@ -513,16 +698,16 @@ const VideoSourceConfig = ({ config }: { config: AdminConfig | null }) => { return (
- {/* 添加数据源表单 */} + {/* 添加视频源表单 */}

- 数据源列表 + 视频源列表

@@ -578,7 +763,7 @@ const VideoSourceConfig = ({ config }: { config: AdminConfig | null }) => {
)} - {/* 数据源表格 */} + {/* 视频源表格 */}
@@ -649,6 +834,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => { SiteInterfaceCacheTime: 7200, SearchResultDefaultAggregate: false, }); + // 保存状态 + const [saving, setSaving] = useState(false); useEffect(() => { if (config?.SiteConfig) { @@ -656,6 +843,43 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => { } }, [config]); + // 保存站点配置 + const handleSave = async () => { + const username = + typeof window !== 'undefined' ? localStorage.getItem('username') : null; + if (!username) { + showError('无法获取用户名,请重新登录'); + return; + } + + const password = + typeof window !== 'undefined' ? localStorage.getItem('password') : null; + if (!password) { + showError('无法获取密码,请重新登录'); + return; + } + + try { + setSaving(true); + const resp = await fetch('/api/admin/site', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password, ...siteSettings }), + }); + + if (!resp.ok) { + const data = await resp.json().catch(() => ({})); + throw new Error(data.error || `保存失败: ${resp.status}`); + } + + showSuccess('保存成功, 请刷新页面'); + } catch (err) { + showError(err instanceof Error ? err.message : '保存失败'); + } finally { + setSaving(false); + } + }; + if (!config) { return (
@@ -767,8 +991,14 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => { {/* 操作按钮 */}
-
@@ -779,6 +1009,7 @@ export default function AdminPage() { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [role, setRole] = useState<'owner' | 'admin' | null>(null); const [expandedTabs, setExpandedTabs] = useState<{ [key: string]: boolean }>({ userConfig: false, videoSource: false, @@ -786,28 +1017,44 @@ export default function AdminPage() { }); // 获取管理员配置 - useEffect(() => { - const fetchConfig = async () => { - try { + // showLoading 用于控制是否在请求期间显示整体加载骨架。 + const fetchConfig = useCallback(async (showLoading = false) => { + try { + if (showLoading) { setLoading(true); - const response = await fetch('/api/admin/config'); + } - if (!response.ok) { - throw new Error(`获取配置失败: ${response.status}`); - } + const username = localStorage.getItem('username'); + const response = await fetch( + `/api/admin/config${ + username ? `?username=${encodeURIComponent(username)}` : '' + }` + ); - const data = (await response.json()) as AdminConfigResult; - setConfig(data.Config); - } catch (err) { - setError(err instanceof Error ? err.message : '获取配置失败'); - } finally { + if (!response.ok) { + const data = (await response.json()) as any; + throw new Error(`获取配置失败: ${data.error}`); + } + + const data = (await response.json()) as AdminConfigResult; + setConfig(data.Config); + setRole(data.Role); + } catch (err) { + const msg = err instanceof Error ? err.message : '获取配置失败'; + showError(msg); + setError(msg); + } finally { + if (showLoading) { setLoading(false); } - }; - - fetchConfig(); + } }, []); + useEffect(() => { + // 首次加载时显示骨架 + fetchConfig(true); + }, [fetchConfig]); + // 切换标签展开状态 const toggleTab = (tabKey: string) => { setExpandedTabs((prev) => ({ @@ -816,6 +1063,48 @@ export default function AdminPage() { })); }; + // 新增: 重置配置处理函数 + const handleResetConfig = async () => { + const username = localStorage.getItem('username'); + if (!username) { + showError('无法获取用户名,请重新登录'); + return; + } + const { isConfirmed } = await Swal.fire({ + title: '确认重置配置', + text: '此操作将重置用户封禁和管理员设置、自定义视频源,站点配置将重置为默认值,是否继续?', + icon: 'warning', + showCancelButton: true, + confirmButtonText: '确认', + cancelButtonText: '取消', + }); + if (!isConfirmed) return; + + const password = localStorage.getItem('password'); + if (!password) { + showError('无法获取密码,请重新登录'); + return; + } + + try { + const response = await fetch( + `/api/admin/reset${ + username + ? `?username=${encodeURIComponent( + username + )}&password=${encodeURIComponent(password)}` + : '' + }` + ); + if (!response.ok) { + throw new Error(`重置失败: ${response.status}`); + } + showSuccess('重置成功,请刷新页面!'); + } catch (err) { + showError(err instanceof Error ? err.message : '重置失败'); + } + }; + if (loading) { return ( @@ -839,29 +1128,28 @@ export default function AdminPage() { } if (error) { - return ( - -
-
-

- 管理员设置 -

-
-

{error}

-
-
-
-
- ); + // 错误已通过 SweetAlert2 展示,此处直接返回空 + return null; } return (
-

- 管理员设置 -

+ {/* 标题 + 重置配置按钮 */} +
+

+ 管理员设置 +

+ {config && role === 'owner' && ( + + )} +
{/* 站点配置标签 */} toggleTab('userConfig')} > - + {/* 视频源配置标签 */} @@ -900,7 +1192,7 @@ export default function AdminPage() { isExpanded={expandedTabs.videoSource} onToggle={() => toggleTab('videoSource')} > - +
diff --git a/src/app/api/admin/config/route.ts b/src/app/api/admin/config/route.ts index 1dd871d..35c584c 100644 --- a/src/app/api/admin/config/route.ts +++ b/src/app/api/admin/config/route.ts @@ -7,13 +7,39 @@ import { getConfig } from '@/lib/config'; export const runtime = 'edge'; -export async function GET() { +export async function GET(request: Request) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { + error: '不支持本地存储进行管理员配置', + }, + { status: 400 } + ); + } + + const { searchParams } = new URL(request.url); + const username = searchParams.get('username'); + try { const config = getConfig(); const result: AdminConfigResult = { Role: 'owner', Config: config, }; + if (username === process.env.USERNAME) { + result.Role = 'owner'; + } else { + const user = config.UserConfig.Users.find((u) => u.username === username); + if (user && user.role === 'admin') { + result.Role = 'admin'; + } else { + return NextResponse.json( + { error: '你是管理员吗你就访问?' }, + { status: 401 } + ); + } + } return NextResponse.json(result, { headers: { @@ -31,30 +57,3 @@ export async function GET() { ); } } - -export async function POST(request: Request) { - try { - const updateData = await request.json(); - - // 在实际应用中,这里应该验证用户权限并更新配置 - console.log('更新管理员配置:', updateData); - - // 模拟配置更新 - // 在实际应用中,这里应该将配置保存到数据库或配置文件 - - return NextResponse.json({ - success: true, - message: '配置更新成功', - timestamp: new Date().toISOString(), - }); - } catch (error) { - console.error('更新管理员配置失败:', error); - return NextResponse.json( - { - error: '更新管理员配置失败', - details: (error as Error).message, - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/admin/reset/route.ts b/src/app/api/admin/reset/route.ts new file mode 100644 index 0000000..8e78d35 --- /dev/null +++ b/src/app/api/admin/reset/route.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ + +import { NextResponse } from 'next/server'; + +import { resetConfig } from '@/lib/config'; + +export const runtime = 'edge'; + +export async function GET(request: Request) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { + error: '不支持本地存储进行管理员配置', + }, + { status: 400 } + ); + } + + const { searchParams } = new URL(request.url); + const username = searchParams.get('username'); + const password = searchParams.get('password'); + + if (!username || !password) { + return NextResponse.json( + { error: '用户名和密码不能为空' }, + { status: 400 } + ); + } + + if (username !== process.env.USERNAME || password !== process.env.PASSWORD) { + return NextResponse.json({ error: '仅支持站长重置配置' }, { status: 401 }); + } + + try { + await resetConfig(); + + return NextResponse.json( + { ok: true }, + { + headers: { + 'Cache-Control': 'no-store', // 管理员配置不缓存 + }, + } + ); + } catch (error) { + return NextResponse.json( + { + error: '重置管理员配置失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/site/route.ts b/src/app/api/admin/site/route.ts new file mode 100644 index 0000000..eb4a362 --- /dev/null +++ b/src/app/api/admin/site/route.ts @@ -0,0 +1,123 @@ +/* eslint-disable @typescript-eslint/no-explicit-any,no-console */ + +import { NextResponse } from 'next/server'; + +import { getConfig } from '@/lib/config'; +import { getStorage } from '@/lib/db'; + +export const runtime = 'edge'; + +export async function POST(request: Request) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { + error: '不支持本地存储进行管理员配置', + }, + { status: 400 } + ); + } + + try { + const body = await request.json(); + + const { + username, + password, + SiteName, + Announcement, + SearchDownstreamMaxPage, + SiteInterfaceCacheTime, + SearchResultDefaultAggregate, + } = body as { + username?: string; + password?: string; + SiteName: string; + Announcement: string; + SearchDownstreamMaxPage: number; + SiteInterfaceCacheTime: number; + SearchResultDefaultAggregate: boolean; + }; + + // 参数校验 + if ( + typeof SiteName !== 'string' || + typeof Announcement !== 'string' || + typeof SearchDownstreamMaxPage !== 'number' || + typeof SiteInterfaceCacheTime !== 'number' || + typeof SearchResultDefaultAggregate !== 'boolean' + ) { + return NextResponse.json({ error: '参数格式错误' }, { status: 400 }); + } + + if (!username || !password) { + return NextResponse.json( + { error: '用户名和密码不能为空' }, + { status: 400 } + ); + } + + const adminConfig = getConfig(); + const storage = getStorage(); + + // 权限与密码校验 + if (username === process.env.USERNAME) { + // 站长 + if (password !== process.env.PASSWORD) { + return NextResponse.json({ error: '密码错误' }, { status: 401 }); + } + } else { + // 管理员 + const user = adminConfig.UserConfig.Users.find( + (u) => u.username === username + ); + if (!user || user.role !== 'admin') { + return NextResponse.json({ error: '权限不足' }, { status: 401 }); + } + + if (!storage || typeof storage.verifyUser !== 'function') { + return NextResponse.json( + { error: '存储未配置用户认证' }, + { status: 500 } + ); + } + + const ok = await storage.verifyUser(username, password); + if (!ok) { + return NextResponse.json({ error: '密码错误' }, { status: 401 }); + } + } + + // 更新缓存中的站点设置 + adminConfig.SiteConfig = { + SiteName, + Announcement, + SearchDownstreamMaxPage, + SiteInterfaceCacheTime, + SearchResultDefaultAggregate, + }; + + // 写入数据库 + if (storage && typeof (storage as any).setAdminConfig === 'function') { + await (storage as any).setAdminConfig(adminConfig); + } + + return NextResponse.json( + { ok: true }, + { + headers: { + 'Cache-Control': 'no-store', // 不缓存结果 + }, + } + ); + } catch (error) { + console.error('更新站点配置失败:', error); + return NextResponse.json( + { + error: '更新站点配置失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/source/route.ts b/src/app/api/admin/source/route.ts new file mode 100644 index 0000000..2b871e9 --- /dev/null +++ b/src/app/api/admin/source/route.ts @@ -0,0 +1,184 @@ +/* eslint-disable @typescript-eslint/no-explicit-any,no-console */ + +import { NextResponse } from 'next/server'; + +import { getConfig } from '@/lib/config'; +import { getStorage, IStorage } from '@/lib/db'; + +export const runtime = 'edge'; + +// 支持的操作类型 +type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort'; + +interface BaseBody { + username?: string; + password?: string; + action?: Action; +} + +export async function POST(request: Request) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { + error: '不支持本地存储进行管理员配置', + }, + { status: 400 } + ); + } + + try { + const body = (await request.json()) as BaseBody & Record; + + const { username, password, action } = body; + + // 基础校验 + const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort']; + if (!username || !password || !action || !ACTIONS.includes(action)) { + return NextResponse.json({ error: '参数格式错误' }, { status: 400 }); + } + + // 获取配置与存储 + const adminConfig = getConfig(); + const storage: IStorage | null = getStorage(); + + // 权限与身份校验 + if (username === process.env.USERNAME) { + if (password !== process.env.PASSWORD) { + return NextResponse.json( + { error: '用户名或密码错误' }, + { status: 401 } + ); + } + } else { + const userEntry = adminConfig.UserConfig.Users.find( + (u) => u.username === username + ); + if (!userEntry || userEntry.role !== 'admin') { + return NextResponse.json({ error: '权限不足' }, { status: 401 }); + } + if (!storage || typeof storage.verifyUser !== 'function') { + return NextResponse.json( + { error: '存储未配置用户认证' }, + { status: 500 } + ); + } + const pass = await storage.verifyUser(username, password); + if (!pass) { + return NextResponse.json( + { error: '用户名或密码错误' }, + { status: 401 } + ); + } + } + + switch (action) { + case 'add': { + const { key, name, api, detail } = body as { + key?: string; + name?: string; + api?: string; + detail?: string; + }; + if (!key || !name || !api) { + return NextResponse.json({ error: '缺少必要参数' }, { status: 400 }); + } + if (adminConfig.SourceConfig.some((s) => s.key === key)) { + return NextResponse.json({ error: '该源已存在' }, { status: 400 }); + } + adminConfig.SourceConfig.push({ + key, + name, + api, + detail, + from: 'custom', + disabled: false, + }); + break; + } + case 'disable': { + const { key } = body as { key?: string }; + if (!key) + return NextResponse.json({ error: '缺少 key 参数' }, { status: 400 }); + const entry = adminConfig.SourceConfig.find((s) => s.key === key); + if (!entry) + return NextResponse.json({ error: '源不存在' }, { status: 404 }); + entry.disabled = true; + break; + } + case 'enable': { + const { key } = body as { key?: string }; + if (!key) + return NextResponse.json({ error: '缺少 key 参数' }, { status: 400 }); + const entry = adminConfig.SourceConfig.find((s) => s.key === key); + if (!entry) + return NextResponse.json({ error: '源不存在' }, { status: 404 }); + entry.disabled = false; + break; + } + case 'delete': { + const { key } = body as { key?: string }; + if (!key) + return NextResponse.json({ error: '缺少 key 参数' }, { status: 400 }); + const idx = adminConfig.SourceConfig.findIndex((s) => s.key === key); + if (idx === -1) + return NextResponse.json({ error: '源不存在' }, { status: 404 }); + const entry = adminConfig.SourceConfig[idx]; + if (entry.from === 'config') { + return NextResponse.json({ error: '该源不可删除' }, { status: 400 }); + } + adminConfig.SourceConfig.splice(idx, 1); + break; + } + case 'sort': { + const { order } = body as { order?: string[] }; + if (!Array.isArray(order)) { + return NextResponse.json( + { error: '排序列表格式错误' }, + { status: 400 } + ); + } + const map = new Map(adminConfig.SourceConfig.map((s) => [s.key, s])); + const newList: typeof adminConfig.SourceConfig = []; + order.forEach((k) => { + const item = map.get(k); + if (item) { + newList.push(item); + map.delete(k); + } + }); + // 未在 order 中的保持原顺序 + adminConfig.SourceConfig.forEach((item) => { + if (map.has(item.key)) newList.push(item); + }); + adminConfig.SourceConfig = newList; + break; + } + default: + return NextResponse.json({ error: '未知操作' }, { status: 400 }); + } + + // 持久化到存储 + if (storage && typeof (storage as any).setAdminConfig === 'function') { + await (storage as any).setAdminConfig(adminConfig); + } + + return NextResponse.json( + { ok: true }, + { + headers: { + 'Cache-Control': 'no-store', + }, + } + ); + } catch (error) { + console.error('视频源管理操作失败:', error); + return NextResponse.json( + { + error: '视频源管理操作失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/admin/user/route.ts b/src/app/api/admin/user/route.ts new file mode 100644 index 0000000..1e4383a --- /dev/null +++ b/src/app/api/admin/user/route.ts @@ -0,0 +1,260 @@ +/* eslint-disable @typescript-eslint/no-explicit-any,no-console,@typescript-eslint/no-non-null-assertion */ + +import { NextResponse } from 'next/server'; + +import { getConfig } from '@/lib/config'; +import { getStorage, IStorage } from '@/lib/db'; + +export const runtime = 'edge'; + +// 支持的操作类型 +const ACTIONS = [ + 'add', + 'ban', + 'unban', + 'setAdmin', + 'cancelAdmin', + 'setAllowRegister', +] as const; + +export async function POST(request: Request) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType === 'localstorage') { + return NextResponse.json( + { + error: '不支持本地存储进行管理员配置', + }, + { status: 400 } + ); + } + + try { + const body = await request.json(); + + const { + username, // 操作者用户名 + password, // 操作者密码 + targetUsername, // 目标用户名 + targetPassword, // 目标用户密码(仅在添加用户时需要) + allowRegister, + action, + } = body as { + username?: string; + password?: string; + targetUsername?: string; + targetPassword?: string; + allowRegister?: boolean; + action?: (typeof ACTIONS)[number]; + }; + + if (!username || !password || !action || !ACTIONS.includes(action)) { + return NextResponse.json({ error: '参数格式错误' }, { status: 400 }); + } + + if (action !== 'setAllowRegister' && !targetUsername) { + return NextResponse.json({ error: '缺少目标用户名' }, { status: 400 }); + } + + if (action !== 'setAllowRegister' && username === targetUsername) { + return NextResponse.json( + { error: '无法对自己进行此操作' }, + { status: 400 } + ); + } + + // 获取配置与存储 + const adminConfig = getConfig(); + const storage: IStorage | null = getStorage(); + + // 判定操作者角色 + let operatorRole: 'owner' | 'admin'; + if (username === process.env.USERNAME) { + operatorRole = 'owner'; + if (password !== process.env.PASSWORD) { + return NextResponse.json( + { error: '用户名或密码错误' }, + { status: 401 } + ); + } + } else { + const userEntry = adminConfig.UserConfig.Users.find( + (u) => u.username === username + ); + if (!userEntry || userEntry.role !== 'admin') { + return NextResponse.json({ error: '权限不足' }, { status: 401 }); + } + operatorRole = 'admin'; + + if (!storage || typeof storage.verifyUser !== 'function') { + return NextResponse.json( + { error: '存储未配置用户认证' }, + { status: 500 } + ); + } + const ok = await storage.verifyUser(username, password); + if (!ok) { + return NextResponse.json( + { error: '用户名或密码错误' }, + { status: 401 } + ); + } + } + + // 查找目标用户条目 + let targetEntry = adminConfig.UserConfig.Users.find( + (u) => u.username === targetUsername + ); + + if (targetEntry && targetEntry.role === 'owner') { + return NextResponse.json({ error: '无法操作站长' }, { status: 400 }); + } + + // 权限校验逻辑 + const isTargetAdmin = targetEntry?.role === 'admin'; + + if (action === 'setAllowRegister') { + if (typeof allowRegister !== 'boolean') { + return NextResponse.json({ error: '参数格式错误' }, { status: 400 }); + } + adminConfig.UserConfig.AllowRegister = allowRegister; + // 保存后直接返回成功(走后面的统一保存逻辑) + } else { + switch (action) { + case 'add': { + if (targetEntry) { + return NextResponse.json({ error: '用户已存在' }, { status: 400 }); + } + if (!targetPassword) { + return NextResponse.json( + { error: '缺少目标用户密码' }, + { status: 400 } + ); + } + if (!storage || typeof storage.registerUser !== 'function') { + return NextResponse.json( + { error: '存储未配置用户注册' }, + { status: 500 } + ); + } + await storage.registerUser(targetUsername!, targetPassword); + // 更新配置 + adminConfig.UserConfig.Users.push({ + username: targetUsername!, + role: 'user', + }); + targetEntry = + adminConfig.UserConfig.Users[ + adminConfig.UserConfig.Users.length - 1 + ]; + break; + } + case 'ban': { + if (!targetEntry) { + return NextResponse.json( + { error: '目标用户不存在' }, + { status: 404 } + ); + } + if (isTargetAdmin) { + // 目标是管理员 + if (operatorRole !== 'owner') { + return NextResponse.json( + { error: '仅站长可封禁管理员' }, + { status: 401 } + ); + } + } + targetEntry.banned = true; + break; + } + case 'unban': { + if (!targetEntry) { + return NextResponse.json( + { error: '目标用户不存在' }, + { status: 404 } + ); + } + if (isTargetAdmin) { + if (operatorRole !== 'owner') { + return NextResponse.json( + { error: '仅站长可操作管理员' }, + { status: 401 } + ); + } + } + targetEntry.banned = false; + break; + } + case 'setAdmin': { + if (!targetEntry) { + return NextResponse.json( + { error: '目标用户不存在' }, + { status: 404 } + ); + } + if (targetEntry.role === 'admin') { + return NextResponse.json( + { error: '该用户已是管理员' }, + { status: 400 } + ); + } + if (operatorRole !== 'owner') { + return NextResponse.json( + { error: '仅站长可设置管理员' }, + { status: 401 } + ); + } + targetEntry.role = 'admin'; + break; + } + case 'cancelAdmin': { + if (!targetEntry) { + return NextResponse.json( + { error: '目标用户不存在' }, + { status: 404 } + ); + } + if (targetEntry.role !== 'admin') { + return NextResponse.json( + { error: '目标用户不是管理员' }, + { status: 400 } + ); + } + if (operatorRole !== 'owner') { + return NextResponse.json( + { error: '仅站长可取消管理员' }, + { status: 401 } + ); + } + targetEntry.role = 'user'; + break; + } + default: + return NextResponse.json({ error: '未知操作' }, { status: 400 }); + } + } + + // 将更新后的配置写入数据库 + if (storage && typeof (storage as any).setAdminConfig === 'function') { + await (storage as any).setAdminConfig(adminConfig); + } + + return NextResponse.json( + { ok: true }, + { + headers: { + 'Cache-Control': 'no-store', // 管理员配置不缓存 + }, + } + ); + } catch (error) { + console.error('用户管理操作失败:', error); + return NextResponse.json( + { + error: '用户管理操作失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index 0bea865..b1f1f25 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { NextRequest, NextResponse } from 'next/server'; +import { getConfig } from '@/lib/config'; import { db } from '@/lib/db'; export const runtime = 'edge'; @@ -44,7 +45,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: '密码不能为空' }, { status: 400 }); } - // 可能是管理员,直接读环境变量 + // 可能是站长,直接读环境变量 if ( username === process.env.USERNAME && password === process.env.PASSWORD @@ -52,6 +53,12 @@ export async function POST(req: NextRequest) { return NextResponse.json({ ok: true }); } + const config = getConfig(); + const user = config.UserConfig.Users.find((u) => u.username === username); + if (user && user.banned) { + return NextResponse.json({ error: '用户被封禁' }, { status: 401 }); + } + // 校验用户密码 try { const pass = await db.verifyUser(username, password); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5f279a8..e0e5880 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from 'next/font/google'; import '../lib/cron'; import './globals.css'; +import 'sweetalert2/dist/sweetalert2.min.css'; import { getConfig } from '@/lib/config'; diff --git a/src/lib/config.ts b/src/lib/config.ts index f5eca3e..63a2a1d 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -43,142 +43,205 @@ export const API_CONFIG = { let fileConfig: ConfigFileStruct; let cachedConfig: AdminConfig; -if (process.env.DOCKER_ENV === 'true') { - // 这里用 eval("require") 避开静态分析,防止 Edge Runtime 打包时报 "Can't resolve 'fs'" - // 在实际 Node.js 运行时才会执行到,因此不会影响 Edge 环境。 - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const _require = eval('require') as NodeRequire; - const fs = _require('fs') as typeof import('fs'); - const path = _require('path') as typeof import('path'); +async function initConfig() { + if (process.env.DOCKER_ENV === 'true') { + // 这里用 eval("require") 避开静态分析,防止 Edge Runtime 打包时报 "Can't resolve 'fs'" + // 在实际 Node.js 运行时才会执行到,因此不会影响 Edge 环境。 + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const _require = eval('require') as NodeRequire; + const fs = _require('fs') as typeof import('fs'); + const path = _require('path') as typeof import('path'); - const configPath = path.join(process.cwd(), 'config.json'); - const raw = fs.readFileSync(configPath, 'utf-8'); - fileConfig = JSON.parse(raw) as ConfigFileStruct; - console.log('load dynamic config success'); -} else { - // 默认使用编译时生成的配置 - fileConfig = runtimeConfig as unknown as ConfigFileStruct; -} -const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; -if (storageType !== 'localstorage') { - // 数据库存储,读取并补全管理员配置 - const storage = getStorage(); - (async () => { - try { - // 尝试从数据库获取管理员配置 - let adminConfig: AdminConfig | null = null; - if (storage && typeof (storage as any).getAdminConfig === 'function') { - adminConfig = await (storage as any).getAdminConfig(); - } - - // 新增:获取所有用户名,用于补全 Users - let userNames: string[] = []; - if (storage && typeof (storage as any).getAllUsers === 'function') { - try { - userNames = await (storage as any).getAllUsers(); - } catch (e) { - console.error('获取用户列表失败:', e); + const configPath = path.join(process.cwd(), 'config.json'); + const raw = fs.readFileSync(configPath, 'utf-8'); + fileConfig = JSON.parse(raw) as ConfigFileStruct; + console.log('load dynamic config success'); + } else { + // 默认使用编译时生成的配置 + fileConfig = runtimeConfig as unknown as ConfigFileStruct; + } + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + if (storageType !== 'localstorage') { + // 数据库存储,读取并补全管理员配置 + const storage = getStorage(); + (async () => { + try { + // 尝试从数据库获取管理员配置 + let adminConfig: AdminConfig | null = null; + if (storage && typeof (storage as any).getAdminConfig === 'function') { + adminConfig = await (storage as any).getAdminConfig(); } - } - const apiSiteEntries = Object.entries(fileConfig.api_site); + // 获取所有用户名,用于补全 Users + let userNames: string[] = []; + if (storage && typeof (storage as any).getAllUsers === 'function') { + try { + userNames = await (storage as any).getAllUsers(); + } catch (e) { + console.error('获取用户列表失败:', e); + } + } - if (adminConfig) { - // 补全 SourceConfig - const existed = new Set( - (adminConfig.SourceConfig || []).map((s) => s.key) - ); - apiSiteEntries.forEach(([key, site]) => { - if (!existed.has(key)) { - adminConfig!.SourceConfig.push({ + // 从文件中获取源信息,用于补全源 + const apiSiteEntries = Object.entries(fileConfig.api_site); + + if (adminConfig) { + // 补全 SourceConfig + const existed = new Set( + (adminConfig.SourceConfig || []).map((s) => s.key) + ); + apiSiteEntries.forEach(([key, site]) => { + if (!existed.has(key)) { + adminConfig!.SourceConfig.push({ + key, + name: site.name, + api: site.api, + detail: site.detail, + from: 'config', + disabled: false, + }); + } + }); + const existedUsers = new Set( + (adminConfig.UserConfig.Users || []).map((u) => u.username) + ); + userNames.forEach((uname) => { + if (!existedUsers.has(uname)) { + adminConfig!.UserConfig.Users.push({ + username: uname, + role: 'user', + }); + } + }); + // 站长 + const ownerUser = process.env.USERNAME; + if (ownerUser) { + adminConfig!.UserConfig.Users = + adminConfig!.UserConfig.Users.filter( + (u) => u.username !== ownerUser + ); + adminConfig!.UserConfig.Users.unshift({ + username: ownerUser, + role: 'owner', + }); + } + } else { + // 数据库中没有配置,创建新的管理员配置 + let allUsers = userNames.map((uname) => ({ + username: uname, + role: 'user', + })); + const ownerUser = process.env.USERNAME; + if (ownerUser) { + allUsers = allUsers.filter((u) => u.username !== ownerUser); + allUsers.unshift({ + username: ownerUser, + role: 'owner', + }); + } + adminConfig = { + SiteConfig: { + SiteName: process.env.SITE_NAME || 'MoonTV', + Announcement: + process.env.NEXT_PUBLIC_ANNOUNCEMENT || + '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', + SearchDownstreamMaxPage: + Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5, + SiteInterfaceCacheTime: fileConfig.cache_time || 7200, + SearchResultDefaultAggregate: + process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false', + }, + UserConfig: { + AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true', + Users: allUsers as any, + }, + SourceConfig: apiSiteEntries.map(([key, site]) => ({ key, name: site.name, api: site.api, detail: site.detail, from: 'config', disabled: false, - }); - } - }); - const existedUsers = new Set( - (adminConfig.UserConfig.Users || []).map((u) => u.username) - ); - userNames.forEach((uname) => { - if (!existedUsers.has(uname)) { - adminConfig!.UserConfig.Users.push({ - username: uname, - role: 'user', - }); - } - }); - // 管理员 - const adminUser = process.env.USERNAME; - if (adminUser) { - adminConfig!.UserConfig.Users = adminConfig!.UserConfig.Users.filter( - (u) => u.username !== adminUser - ); - adminConfig!.UserConfig.Users.unshift({ - username: adminUser, - role: 'owner', - }); + })), + }; } - } else { - // 数据库中没有配置,创建新的管理员配置 - let allUsers = userNames.map((uname) => ({ - username: uname, - role: 'user', - })); - const adminUser = process.env.USERNAME; - if (adminUser) { - allUsers = allUsers.filter((u) => u.username !== adminUser); - allUsers.unshift({ - username: adminUser, - role: 'owner', - }); + + // 写回数据库(更新/创建) + if (storage && typeof (storage as any).setAdminConfig === 'function') { + await (storage as any).setAdminConfig(adminConfig); } - adminConfig = { - SiteConfig: { - SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV', - Announcement: - process.env.NEXT_PUBLIC_ANNOUNCEMENT || - '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', - SearchDownstreamMaxPage: - Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5, - SiteInterfaceCacheTime: fileConfig.cache_time || 7200, - SearchResultDefaultAggregate: - process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false', - }, - UserConfig: { - AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true', - Users: allUsers as any, - }, - SourceConfig: apiSiteEntries.map(([key, site]) => ({ - key, - name: site.name, - api: site.api, - detail: site.detail, - from: 'config', - disabled: false, - })), - }; - } - // 写回数据库(更新/创建) - if (storage && typeof (storage as any).setAdminConfig === 'function') { - await (storage as any).setAdminConfig(adminConfig); + // 更新缓存 + cachedConfig = adminConfig; + } catch (err) { + console.error('加载管理员配置失败:', err); } + })(); + } else { + // 本地存储直接使用文件配置 + cachedConfig = { + SiteConfig: { + SiteName: process.env.SITE_NAME || 'MoonTV', + Announcement: + process.env.NEXT_PUBLIC_ANNOUNCEMENT || + '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', + SearchDownstreamMaxPage: + Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5, + SiteInterfaceCacheTime: fileConfig.cache_time || 7200, + SearchResultDefaultAggregate: + process.env.NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT !== 'false', + }, + UserConfig: { + AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true', + Users: [], + }, + SourceConfig: Object.entries(fileConfig.api_site).map(([key, site]) => ({ + key, + name: site.name, + api: site.api, + detail: site.detail, + from: 'config', + disabled: false, + })), + } as AdminConfig; + } +} - // 更新缓存 - cachedConfig = adminConfig; - } catch (err) { - console.error('加载管理员配置失败:', err); +initConfig(); + +export function getConfig(): AdminConfig { + return cachedConfig; +} + +export async function resetConfig() { + const storage = getStorage(); + // 获取所有用户名,用于补全 Users + let userNames: string[] = []; + if (storage && typeof (storage as any).getAllUsers === 'function') { + try { + userNames = await (storage as any).getAllUsers(); + } catch (e) { + console.error('获取用户列表失败:', e); } - })(); -} else { - // 本地存储直接使用文件配置 - cachedConfig = { + } + + // 从文件中获取源信息,用于补全源 + const apiSiteEntries = Object.entries(fileConfig.api_site); + let allUsers = userNames.map((uname) => ({ + username: uname, + role: 'user', + })); + const ownerUser = process.env.USERNAME; + if (ownerUser) { + allUsers = allUsers.filter((u) => u.username !== ownerUser); + allUsers.unshift({ + username: ownerUser, + role: 'owner', + }); + } + const adminConfig = { SiteConfig: { - SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV', + SiteName: process.env.SITE_NAME || 'MoonTV', Announcement: process.env.NEXT_PUBLIC_ANNOUNCEMENT || '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', @@ -190,9 +253,9 @@ if (storageType !== 'localstorage') { }, UserConfig: { AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true', - Users: [], + Users: allUsers as any, }, - SourceConfig: Object.entries(fileConfig.api_site).map(([key, site]) => ({ + SourceConfig: apiSiteEntries.map(([key, site]) => ({ key, name: site.name, api: site.api, @@ -201,10 +264,14 @@ if (storageType !== 'localstorage') { disabled: false, })), } as AdminConfig; -} -export function getConfig(): AdminConfig { - return cachedConfig; + if (storage && typeof (storage as any).setAdminConfig === 'function') { + await (storage as any).setAdminConfig(adminConfig); + } + + cachedConfig.SiteConfig = adminConfig.SiteConfig; + cachedConfig.UserConfig = adminConfig.UserConfig; + cachedConfig.SourceConfig = adminConfig.SourceConfig; } export function getCacheTime(): number { diff --git a/start.js b/start.js new file mode 100644 index 0000000..593137e --- /dev/null +++ b/start.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +/* eslint-disable no-console,@typescript-eslint/no-var-requires */ +const http = require('http'); + +// 直接在当前进程中启动 standalone Server(`server.js`) +require('./server.js'); + +// 每 1 秒轮询一次,直到请求成功 +const TARGET_URL = `http://${process.env.HOSTNAME || 'localhost'}:${ + process.env.PORT || 3000 +}/login`; + +const intervalId = setInterval(() => { + console.log(`Fetching ${TARGET_URL} ...`); + + const req = http.get(TARGET_URL, (res) => { + // 当返回 2xx 状态码时认为成功,然后停止轮询 + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + console.log('Server is up, stop polling.'); + clearInterval(intervalId); + } + }); + + req.setTimeout(2000, () => { + req.destroy(); + }); +}, 1000);