feat: admin api cookie

This commit is contained in:
shinya
2025-07-09 22:42:53 +08:00
parent 7edc673c9f
commit 2bbb67ac9c
16 changed files with 215 additions and 384 deletions

View File

@@ -27,6 +27,7 @@ import { Suspense, useCallback, useEffect, useState } from 'react';
import Swal from 'sweetalert2';
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import PageLayout from '@/components/PageLayout';
@@ -118,8 +119,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
});
// 当前登录用户名
const currentUsername =
typeof window !== 'undefined' ? localStorage.getItem('username') : null;
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
useEffect(() => {
if (config?.UserConfig) {
@@ -131,15 +131,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
// 切换允许注册设置
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 }));
@@ -148,8 +139,6 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
password,
action: 'setAllowRegister',
allowRegister: value,
}),
@@ -197,23 +186,11 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
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,
@@ -521,21 +498,11 @@ const VideoSourceConfig = ({
// 通用 API 请求
const callSourceApi = async (body: Record<string, any>) => {
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 }),
body: JSON.stringify({ ...body }),
});
if (!resp.ok) {
@@ -845,26 +812,12 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
// 保存站点配置
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 }),
body: JSON.stringify({ ...siteSettings }),
});
if (!resp.ok) {
@@ -1024,12 +977,7 @@ function AdminPageClient() {
setLoading(true);
}
const username = localStorage.getItem('username');
const response = await fetch(
`/api/admin/config${
username ? `?username=${encodeURIComponent(username)}` : ''
}`
);
const response = await fetch(`/api/admin/config`);
if (!response.ok) {
const data = (await response.json()) as any;
@@ -1065,11 +1013,6 @@ function AdminPageClient() {
// 新增: 重置配置处理函数
const handleResetConfig = async () => {
const username = localStorage.getItem('username');
if (!username) {
showError('无法获取用户名,请重新登录');
return;
}
const { isConfirmed } = await Swal.fire({
title: '确认重置配置',
text: '此操作将重置用户封禁和管理员设置、自定义视频源,站点配置将重置为默认值,是否继续?',
@@ -1080,22 +1023,8 @@ function AdminPageClient() {
});
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)}`
: ''
}`
);
const response = await fetch(`/api/admin/reset`);
if (!response.ok) {
throw new Error(`重置失败: ${response.status}`);
}

View File

@@ -1,13 +1,14 @@
/* eslint-disable no-console */
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { AdminConfigResult } from '@/lib/admin.types';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
export const runtime = 'edge';
export async function GET(request: Request) {
export async function GET(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return NextResponse.json(
@@ -18,8 +19,11 @@ export async function GET(request: Request) {
);
}
const { searchParams } = new URL(request.url);
const username = searchParams.get('username');
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
try {
const config = getConfig();

View File

@@ -1,12 +1,13 @@
/* eslint-disable no-console */
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { resetConfig } from '@/lib/config';
export const runtime = 'edge';
export async function GET(request: Request) {
export async function GET(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return NextResponse.json(
@@ -17,18 +18,13 @@ export async function GET(request: Request) {
);
}
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 }
);
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
if (username !== process.env.USERNAME || password !== process.env.PASSWORD) {
if (username !== process.env.USERNAME) {
return NextResponse.json({ error: '仅支持站长重置配置' }, { status: 401 });
}

View File

@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { getStorage } from '@/lib/db';
export const runtime = 'edge';
export async function POST(request: Request) {
export async function POST(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return NextResponse.json(
@@ -21,17 +22,19 @@ export async function POST(request: Request) {
try {
const body = await request.json();
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
const {
username,
password,
SiteName,
Announcement,
SearchDownstreamMaxPage,
SiteInterfaceCacheTime,
SearchResultDefaultAggregate,
} = body as {
username?: string;
password?: string;
SiteName: string;
Announcement: string;
SearchDownstreamMaxPage: number;
@@ -50,23 +53,11 @@ export async function POST(request: Request) {
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 {
// 权限校验
if (username !== process.env.USERNAME) {
// 管理员
const user = adminConfig.UserConfig.Users.find(
(u) => u.username === username
@@ -74,18 +65,6 @@ export async function POST(request: Request) {
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 });
}
}
// 更新缓存中的站点设置

View File

@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { getStorage } from '@/lib/db';
import { IStorage } from '@/lib/types';
@@ -12,12 +13,10 @@ 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) {
export async function POST(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return NextResponse.json(
@@ -30,12 +29,17 @@ export async function POST(request: Request) {
try {
const body = (await request.json()) as BaseBody & Record<string, any>;
const { action } = body;
const { username, password, action } = body;
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
// 基础校验
const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort'];
if (!username || !password || !action || !ACTIONS.includes(action)) {
if (!username || !action || !ACTIONS.includes(action)) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -44,33 +48,13 @@ export async function POST(request: Request) {
const storage: IStorage | null = getStorage();
// 权限与身份校验
if (username === process.env.USERNAME) {
if (password !== process.env.PASSWORD) {
return NextResponse.json(
{ error: '用户名或密码错误' },
{ status: 401 }
);
}
} else {
if (username !== process.env.USERNAME) {
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) {

View File

@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any,no-console,@typescript-eslint/no-non-null-assertion */
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { getStorage } from '@/lib/db';
import { IStorage } from '@/lib/types';
@@ -18,7 +19,7 @@ const ACTIONS = [
'setAllowRegister',
] as const;
export async function POST(request: Request) {
export async function POST(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return NextResponse.json(
@@ -32,23 +33,25 @@ export async function POST(request: Request) {
try {
const body = await request.json();
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
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)) {
if (!action || !ACTIONS.includes(action)) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -71,12 +74,6 @@ export async function POST(request: Request) {
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
@@ -85,20 +82,6 @@ export async function POST(request: Request) {
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 }
);
}
}
// 查找目标用户条目

View File

@@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { db } from '@/lib/db';
import { Favorite } from '@/lib/types';
@@ -16,13 +17,14 @@ export const runtime = 'edge';
*/
export async function GET(request: NextRequest) {
try {
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const key = searchParams.get('key');
const user = searchParams.get('user');
if (!user) {
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
}
// 查询单条收藏
if (key) {
@@ -33,12 +35,12 @@ export async function GET(request: NextRequest) {
{ status: 400 }
);
}
const fav = await db.getFavorite(user, source, id);
const fav = await db.getFavorite(authInfo.username, source, id);
return NextResponse.json(fav, { status: 200 });
}
// 查询全部收藏
const favorites = await db.getAllFavorites(user);
const favorites = await db.getAllFavorites(authInfo.username);
return NextResponse.json(favorites, { status: 200 });
} catch (err) {
console.error('获取收藏失败', err);
@@ -51,21 +53,19 @@ export async function GET(request: NextRequest) {
/**
* POST /api/favorites
* body: { user?: string; key: string; favorite: Favorite }
* body: { key: string; favorite: Favorite }
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
user,
key,
favorite,
}: { user?: string; key: string; favorite: Favorite } = body;
if (!user) {
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { key, favorite }: { key: string; favorite: Favorite } = body;
if (!key || !favorite) {
return NextResponse.json(
{ error: 'Missing key or favorite' },
@@ -94,7 +94,7 @@ export async function POST(request: NextRequest) {
save_time: favorite.save_time ?? Date.now(),
} as Omit<Favorite, 'user_id'>;
await db.saveFavorite(user, source, id, favoriteWithoutUserId);
await db.saveFavorite(authInfo.username, source, id, favoriteWithoutUserId);
return NextResponse.json({ success: true }, { status: 200 });
} catch (err) {
@@ -114,13 +114,15 @@ export async function POST(request: NextRequest) {
*/
export async function DELETE(request: NextRequest) {
try {
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
const { searchParams } = new URL(request.url);
const key = searchParams.get('key');
const user = searchParams.get('user');
if (!user) {
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
}
if (key) {
// 删除单条
@@ -131,14 +133,14 @@ export async function DELETE(request: NextRequest) {
{ status: 400 }
);
}
await db.deleteFavorite(user, source, id);
await db.deleteFavorite(username, source, id);
} else {
// 清空全部
const all = await db.getAllFavorites(user);
const all = await db.getAllFavorites(username);
await Promise.all(
Object.keys(all).map(async (k) => {
const [s, i] = k.split('+');
if (s && i) await db.deleteFavorite(user, s, i);
if (s && i) await db.deleteFavorite(username, s, i);
})
);
}

View File

@@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { db } from '@/lib/db';
import { PlayRecord } from '@/lib/types';
@@ -9,13 +10,13 @@ export const runtime = 'edge';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const user = searchParams.get('user');
if (!user) {
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const records = await db.getAllPlayRecords(user);
const records = await db.getAllPlayRecords(authInfo.username);
return NextResponse.json(records, { status: 200 });
} catch (err) {
console.error('获取播放记录失败', err);
@@ -28,17 +29,15 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
user,
key,
record,
}: { user?: string; key: string; record: PlayRecord } = body;
if (!user) {
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { key, record }: { key: string; record: PlayRecord } = body;
if (!key || !record) {
return NextResponse.json(
{ error: 'Missing key or record' },
@@ -75,7 +74,7 @@ export async function POST(request: NextRequest) {
save_time: record.save_time,
};
await db.savePlayRecord(user, source, id, recordWithoutUserId);
await db.savePlayRecord(authInfo.username, source, id, recordWithoutUserId);
return NextResponse.json({ success: true }, { status: 200 });
} catch (err) {
@@ -89,13 +88,15 @@ export async function POST(request: NextRequest) {
export async function DELETE(request: NextRequest) {
try {
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
const { searchParams } = new URL(request.url);
const key = searchParams.get('key');
const user = searchParams.get('user');
if (!user) {
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
}
if (key) {
// 如果提供了 key删除单条播放记录
@@ -107,15 +108,15 @@ export async function DELETE(request: NextRequest) {
);
}
await db.deletePlayRecord(user, source, id);
await db.deletePlayRecord(username, source, id);
} else {
// 未提供 key则清空全部播放记录
// 目前 DbManager 没有对应方法,这里直接遍历删除
const all = await db.getAllPlayRecords(user);
const all = await db.getAllPlayRecords(username);
await Promise.all(
Object.keys(all).map(async (k) => {
const [s, i] = k.split('+');
if (s && i) await db.deletePlayRecord(user, s, i);
if (s && i) await db.deletePlayRecord(username, s, i);
})
);
}

View File

@@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { db } from '@/lib/db';
export const runtime = 'edge';
@@ -10,22 +11,18 @@ export const runtime = 'edge';
const HISTORY_LIMIT = 20;
/**
* GET /api/searchhistory?user=<username>
* GET /api/searchhistory
* 返回 string[]
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const user = searchParams.get('user')?.trim();
if (!user) {
return NextResponse.json(
{ error: 'User parameter is required' },
{ status: 400 }
);
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const history = await db.getSearchHistory(user);
const history = await db.getSearchHistory(authInfo.username);
return NextResponse.json(history, { status: 200 });
} catch (err) {
console.error('获取搜索历史失败', err);
@@ -38,13 +35,18 @@ export async function GET(request: NextRequest) {
/**
* POST /api/searchhistory
* body: { keyword: string, user: string }
* body: { keyword: string }
*/
export async function POST(request: NextRequest) {
try {
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const keyword: string = body.keyword?.trim();
const user: string = body.user?.trim();
if (!keyword) {
return NextResponse.json(
@@ -53,17 +55,10 @@ export async function POST(request: NextRequest) {
);
}
if (!user) {
return NextResponse.json(
{ error: 'User parameter is required' },
{ status: 400 }
);
}
await db.addSearchHistory(user, keyword);
await db.addSearchHistory(authInfo.username, keyword);
// 再次获取最新列表,确保客户端与服务端同步
const history = await db.getSearchHistory(user);
const history = await db.getSearchHistory(authInfo.username);
return NextResponse.json(history.slice(0, HISTORY_LIMIT), { status: 200 });
} catch (err) {
console.error('添加搜索历史失败', err);
@@ -75,25 +70,23 @@ export async function POST(request: NextRequest) {
}
/**
* DELETE /api/searchhistory?user=<username>&keyword=<kw>
* DELETE /api/searchhistory?keyword=<kw>
*
* 1. 不带 keyword -> 清空全部搜索历史
* 2. 带 keyword=<kw> -> 删除单条关键字
*/
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const user = searchParams.get('user')?.trim();
const kw = searchParams.get('keyword')?.trim();
if (!user) {
return NextResponse.json(
{ error: 'User parameter is required' },
{ status: 400 }
);
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
await db.deleteSearchHistory(user, kw || undefined);
const { searchParams } = new URL(request.url);
const kw = searchParams.get('keyword')?.trim();
await db.deleteSearchHistory(authInfo.username, kw || undefined);
return NextResponse.json({ success: true }, { status: 200 });
} catch (err) {

View File

@@ -46,14 +46,6 @@ function LoginPageClient() {
});
if (res.ok) {
// API 已经设置了认证cookie这里只保存到localStorage用于向后兼容
if (typeof window !== 'undefined') {
localStorage.setItem('password', password);
if (shouldAskUsername) {
localStorage.setItem('username', username);
}
}
const redirect = searchParams.get('redirect') || '/';
router.replace(redirect);
} else if (res.status === 401) {
@@ -83,12 +75,6 @@ function LoginPageClient() {
});
if (res.ok) {
// API 已经设置了认证cookie这里只保存到localStorage用于向后兼容
if (typeof window !== 'undefined') {
localStorage.setItem('password', password);
localStorage.setItem('username', username);
}
const redirect = searchParams.get('redirect') || '/';
router.replace(redirect);
} else {

View File

@@ -23,12 +23,6 @@ export const LogoutButton: React.FC = () => {
console.error('注销请求失败:', error);
}
// 清除localStorage中的认证信息向后兼容
if (typeof window !== 'undefined') {
localStorage.removeItem('password');
localStorage.removeItem('username');
}
window.location.reload();
};

57
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,57 @@
import { NextRequest } from 'next/server';
// 从cookie获取认证信息 (服务端使用)
export function getAuthInfoFromCookie(request: NextRequest): {
password?: string;
username?: string;
signature?: string;
timestamp?: number;
} | null {
const authCookie = request.cookies.get('auth');
if (!authCookie) {
return null;
}
try {
const decoded = decodeURIComponent(authCookie.value);
const authData = JSON.parse(decoded);
return authData;
} catch (error) {
return null;
}
}
// 从cookie获取认证信息 (客户端使用)
export function getAuthInfoFromBrowserCookie(): {
password?: string;
username?: string;
signature?: string;
timestamp?: number;
} | null {
if (typeof window === 'undefined') {
return null;
}
try {
// 解析 document.cookie
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
if (key && value) {
acc[key] = value;
}
return acc;
}, {} as Record<string, string>);
const authCookie = cookies['auth'];
if (!authCookie) {
return null;
}
const decoded = decodeURIComponent(authCookie);
const authData = JSON.parse(decoded);
return authData;
} catch (error) {
return null;
}
}

View File

@@ -24,27 +24,11 @@ export interface PlayRecord {
play_time: number; // 播放进度(秒)
total_time: number; // 总进度(秒)
save_time: number; // 记录保存时间(时间戳)
user_id: number; // 用户 ID本地存储情况下恒为 0
}
// ---- 常量 ----
const PLAY_RECORDS_KEY = 'moontv_play_records';
// +++ 新增:获取当前用户名工具函数 +++
/**
* 从 localStorage 中读取当前用户名
* 如果不存在则返回 undefined
*/
function getUsername(): string | undefined {
if (typeof window === 'undefined') return undefined;
try {
const name = localStorage.getItem('username')?.trim();
return name || undefined;
} catch {
return undefined;
}
}
// ---- 环境变量 ----
const STORAGE_TYPE = (() => {
const raw =
@@ -84,10 +68,7 @@ export function generateStorageKey(source: string, id: string): string {
export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
// 若配置标明使用数据库,则从后端 API 拉取
if (STORAGE_TYPE !== 'localstorage') {
const user = getUsername();
return fetchFromApi<Record<string, PlayRecord>>(
`/api/playrecords?user=${encodeURIComponent(user ?? '')}`
);
return fetchFromApi<Record<string, PlayRecord>>(`/api/playrecords`);
}
// 默认 / localstorage 流程
@@ -112,21 +93,19 @@ export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
export async function savePlayRecord(
source: string,
id: string,
record: Omit<PlayRecord, 'user_id'>
record: PlayRecord
): Promise<void> {
const key = generateStorageKey(source, id);
const fullRecord: PlayRecord = { ...record, user_id: 0 };
// 若配置标明使用数据库,则通过 API 保存
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
const res = await fetch('/api/playrecords', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user, key, record: fullRecord }),
body: JSON.stringify({ key, record }),
});
if (!res.ok) throw new Error(`保存播放记录失败: ${res.status}`);
} catch (err) {
@@ -144,7 +123,7 @@ export async function savePlayRecord(
try {
const allRecords = await getAllPlayRecords();
allRecords[key] = fullRecord;
allRecords[key] = record;
localStorage.setItem(PLAY_RECORDS_KEY, JSON.stringify(allRecords));
} catch (err) {
console.error('保存播放记录失败:', err);
@@ -165,9 +144,7 @@ export async function deletePlayRecord(
if (STORAGE_TYPE !== 'localstorage') {
try {
const res = await fetch(
`/api/playrecords?key=${encodeURIComponent(
key
)}&user=${encodeURIComponent(getUsername() ?? '')}`,
`/api/playrecords?key=${encodeURIComponent(key)}`,
{
method: 'DELETE',
}
@@ -206,10 +183,7 @@ export async function getSearchHistory(): Promise<string[]> {
// 如果配置为使用数据库,则从后端 API 获取
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
return fetchFromApi<string[]>(
`/api/searchhistory?user=${encodeURIComponent(user ?? '')}`
);
return fetchFromApi<string[]>(`/api/searchhistory`);
} catch (err) {
console.error('获取搜索历史失败:', err);
return [];
@@ -243,13 +217,12 @@ export async function addSearchHistory(keyword: string): Promise<void> {
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
await fetch('/api/searchhistory', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ keyword: trimmed, user: user ?? '' }),
body: JSON.stringify({ keyword: trimmed }),
});
} catch (err) {
console.error('保存搜索历史失败:', err);
@@ -280,8 +253,7 @@ export async function clearSearchHistory(): Promise<void> {
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
await fetch(`/api/searchhistory?user=${encodeURIComponent(user ?? '')}`, {
await fetch(`/api/searchhistory`, {
method: 'DELETE',
});
} catch (err) {
@@ -305,15 +277,9 @@ export async function deleteSearchHistory(keyword: string): Promise<void> {
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
await fetch(
`/api/searchhistory?user=${encodeURIComponent(
user ?? ''
)}&keyword=${encodeURIComponent(trimmed)}`,
{
method: 'DELETE',
}
);
await fetch(`/api/searchhistory?keyword=${encodeURIComponent(trimmed)}`, {
method: 'DELETE',
});
} catch (err) {
console.error('删除搜索历史失败:', err);
}
@@ -342,7 +308,6 @@ export interface Favorite {
cover: string;
total_episodes: number;
save_time: number;
user_id: number; // 本地存储情况下恒为 0
}
// 收藏在 localStorage 中使用的 key
@@ -354,10 +319,7 @@ const FAVORITES_KEY = 'moontv_favorites';
export async function getAllFavorites(): Promise<Record<string, Favorite>> {
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
const user = getUsername();
return fetchFromApi<Record<string, Favorite>>(
`/api/favorites?user=${encodeURIComponent(user ?? '')}`
);
return fetchFromApi<Record<string, Favorite>>(`/api/favorites`);
}
// localStorage 模式
@@ -381,21 +343,19 @@ export async function getAllFavorites(): Promise<Record<string, Favorite>> {
export async function saveFavorite(
source: string,
id: string,
favorite: Omit<Favorite, 'user_id'>
favorite: Favorite
): Promise<void> {
const key = generateStorageKey(source, id);
const fullFavorite: Favorite = { ...favorite, user_id: 0 };
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
const res = await fetch('/api/favorites', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user, key, favorite: fullFavorite }),
body: JSON.stringify({ key, favorite }),
});
if (!res.ok) throw new Error(`保存收藏失败: ${res.status}`);
} catch (err) {
@@ -413,7 +373,7 @@ export async function saveFavorite(
try {
const allFavorites = await getAllFavorites();
allFavorites[key] = fullFavorite;
allFavorites[key] = favorite;
localStorage.setItem(FAVORITES_KEY, JSON.stringify(allFavorites));
} catch (err) {
console.error('保存收藏失败:', err);
@@ -433,15 +393,9 @@ export async function deleteFavorite(
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
const res = await fetch(
`/api/favorites?key=${encodeURIComponent(
key
)}&user=${encodeURIComponent(user ?? '')}`,
{
method: 'DELETE',
}
);
const res = await fetch(`/api/favorites?key=${encodeURIComponent(key)}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error(`删除收藏失败: ${res.status}`);
} catch (err) {
console.error('删除收藏到数据库失败:', err);
@@ -478,12 +432,7 @@ export async function isFavorited(
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
try {
const user = getUsername();
const res = await fetch(
`/api/favorites?key=${encodeURIComponent(
key
)}&user=${encodeURIComponent(user ?? '')}`
);
const res = await fetch(`/api/favorites?key=${encodeURIComponent(key)}`);
if (!res.ok) return false;
const data = await res.json();
return !!data;
@@ -505,7 +454,7 @@ export async function isFavorited(
export async function toggleFavorite(
source: string,
id: string,
favoriteData?: Omit<Favorite, 'user_id'>
favoriteData?: Favorite
): Promise<boolean> {
const already = await isFavorited(source, id);
@@ -528,9 +477,8 @@ export async function toggleFavorite(
export async function clearAllPlayRecords(): Promise<void> {
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
const user = getUsername();
try {
await fetch(`/api/playrecords?user=${encodeURIComponent(user ?? '')}`, {
await fetch(`/api/playrecords`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
@@ -551,9 +499,8 @@ export async function clearAllPlayRecords(): Promise<void> {
export async function clearAllFavorites(): Promise<void> {
// 数据库模式
if (STORAGE_TYPE !== 'localstorage') {
const user = getUsername();
try {
await fetch(`/api/favorites?user=${encodeURIComponent(user ?? '')}`, {
await fetch(`/api/favorites`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});

View File

@@ -63,8 +63,7 @@ export class DbManager {
record: Omit<PlayRecord, 'user_id'>
): Promise<void> {
const key = generateStorageKey(source, id);
const fullRecord: PlayRecord = { ...record, user_id: 0 };
await this.storage.setPlayRecord(userName, key, fullRecord);
await this.storage.setPlayRecord(userName, key, record);
}
async getAllPlayRecords(userName: string): Promise<{
@@ -96,11 +95,10 @@ export class DbManager {
userName: string,
source: string,
id: string,
favorite: Omit<Favorite, 'user_id'>
favorite: Favorite
): Promise<void> {
const key = generateStorageKey(source, id);
const fullFavorite: Favorite = { ...favorite, user_id: 0 };
await this.storage.setFavorite(userName, key, fullFavorite);
await this.storage.setFavorite(userName, key, favorite);
}
async getAllFavorites(

View File

@@ -10,7 +10,6 @@ export interface PlayRecord {
play_time: number; // 播放进度(秒)
total_time: number; // 总进度(秒)
save_time: number; // 记录保存时间(时间戳)
user_id: number; // 用户IDlocalStorage情况下全部为0
}
// 收藏数据结构
@@ -19,7 +18,6 @@ export interface Favorite {
total_episodes: number; // 总集数
title: string;
cover: string;
user_id: number; // 用户IDlocalStorage情况下全部为0
save_time: number; // 记录保存时间(时间戳)
}

View File

@@ -2,6 +2,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
@@ -94,28 +96,6 @@ async function verifySignature(
}
}
// 从cookie获取认证信息
function getAuthInfoFromCookie(request: NextRequest): {
password?: string;
username?: string;
signature?: string;
timestamp?: number;
} | null {
const authCookie = request.cookies.get('auth');
if (!authCookie) {
return null;
}
try {
const decoded = decodeURIComponent(authCookie.value);
const authData = JSON.parse(decoded);
return authData;
} catch (error) {
return null;
}
}
// 重定向到登录页面
function redirectToLogin(request: NextRequest, pathname: string): NextResponse {
const loginUrl = new URL('/login', request.url);