refactor: 完成静态网站转换,移除所有 CMS 和动态功能
- 删除数据库相关代码 (src/db/) - 删除 API 路由 (src/app/api/) - 删除认证相关代码 (src/lib/auth/, src/providers/) - 删除监控和安全中间件 (src/lib/security/, src/lib/monitoring/) - 删除 hooks (use-news, use-products, use-services) - 更新组件为静态数据源 - 添加 nginx 静态配置和部署脚本 - 添加 static-link 组件
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
import { GET, POST, PUT } from './route';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/permissions', () => ({
|
||||
hasPermission: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/check-permission', () => ({
|
||||
checkIsAdmin: jest.fn(),
|
||||
getAdminUserId: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/db', () => {
|
||||
const mockSelect = jest.fn().mockReturnValue({
|
||||
from: jest.fn().mockReturnValue({
|
||||
where: jest.fn().mockReturnValue({
|
||||
limit: jest.fn().mockResolvedValue([]),
|
||||
orderBy: jest.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const mockUpdate = jest.fn().mockReturnValue({
|
||||
set: jest.fn().mockReturnValue({
|
||||
where: jest.fn().mockReturnValue({
|
||||
returning: jest.fn().mockResolvedValue([{
|
||||
id: 'test-id',
|
||||
key: 'test_key',
|
||||
value: 'test_value',
|
||||
category: 'general',
|
||||
}]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
db: {
|
||||
select: mockSelect,
|
||||
update: mockUpdate,
|
||||
insert: jest.fn().mockReturnValue({
|
||||
values: jest.fn().mockReturnValue({
|
||||
returning: jest.fn().mockResolvedValue([{
|
||||
id: 'test-id',
|
||||
key: 'test_key',
|
||||
value: 'test_value',
|
||||
category: 'general',
|
||||
}]),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { checkIsAdmin: mockCheckIsAdmin, getAdminUserId: mockGetAdminUserId } = require('@/lib/auth/check-permission');
|
||||
|
||||
describe('/api/admin/config', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return configs if authenticated and has permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.configs).toBeDefined();
|
||||
expect(Array.isArray(data.configs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: 'test', value: {} }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 400 if missing required fields', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
mockGetAdminUserId.mockResolvedValueOnce('1');
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: 'test' }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe('缺少必要字段');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ configs: [] }),
|
||||
});
|
||||
const response = await PUT(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ configs: [] }),
|
||||
});
|
||||
const response = await PUT(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 400 if configs is not an array', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
mockGetAdminUserId.mockResolvedValueOnce('1');
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ configs: 'not-array' }),
|
||||
});
|
||||
const response = await PUT(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe('无效的数据格式');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { siteConfig } from '@/db/schema';
|
||||
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||
import { forbidden, success, notFound, validationError, badRequest, handleApiError } from '@/lib/api-response';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const category = searchParams.get('category');
|
||||
const key = searchParams.get('key');
|
||||
|
||||
if (key) {
|
||||
const config = await db
|
||||
.select()
|
||||
.from(siteConfig)
|
||||
.where(eq(siteConfig.key, key))
|
||||
.limit(1);
|
||||
|
||||
if (config.length === 0) {
|
||||
return notFound('配置不存在');
|
||||
}
|
||||
|
||||
return success(config[0]);
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (category) {
|
||||
conditions.push(eq(siteConfig.category, category as 'feature' | 'style' | 'seo' | 'general'));
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const configs = await db
|
||||
.select()
|
||||
.from(siteConfig)
|
||||
.where(whereClause)
|
||||
.orderBy(siteConfig.key);
|
||||
|
||||
return success({
|
||||
configs: configs,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { key, value, category, description } = body;
|
||||
|
||||
if (!key || !value || !category) {
|
||||
return validationError('缺少必要字段', { required: ['key', 'value', 'category'] });
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(siteConfig)
|
||||
.where(eq(siteConfig.key, key))
|
||||
.limit(1);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (existing.length > 0) {
|
||||
const updated = await db
|
||||
.update(siteConfig)
|
||||
.set({
|
||||
value,
|
||||
description: description || existing[0]!.description,
|
||||
updatedAt: now,
|
||||
updatedBy: userId,
|
||||
})
|
||||
.where(eq(siteConfig.key, key))
|
||||
.returning();
|
||||
|
||||
return success(updated[0], 200);
|
||||
}
|
||||
|
||||
const newConfig = await db
|
||||
.insert(siteConfig)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
key,
|
||||
value,
|
||||
category,
|
||||
description: description || null,
|
||||
updatedAt: now,
|
||||
updatedBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return success(newConfig[0], 201);
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { configs } = body as { configs: Array<{ key: string; value: unknown; description?: string }> };
|
||||
|
||||
if (!Array.isArray(configs)) {
|
||||
return badRequest('无效的数据格式');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const results = [];
|
||||
|
||||
for (const config of configs) {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(siteConfig)
|
||||
.where(eq(siteConfig.key, config.key))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
const updated = await db
|
||||
.update(siteConfig)
|
||||
.set({
|
||||
value: config.value,
|
||||
description: config.description || existing[0]!.description,
|
||||
updatedAt: now,
|
||||
updatedBy: userId,
|
||||
})
|
||||
.where(eq(siteConfig.key, config.key))
|
||||
.returning();
|
||||
results.push(updated[0]);
|
||||
} else {
|
||||
const created = await db
|
||||
.insert(siteConfig)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
key: config.key,
|
||||
value: config.value,
|
||||
category: 'general',
|
||||
description: config.description || null,
|
||||
updatedAt: now,
|
||||
updatedBy: userId,
|
||||
})
|
||||
.returning();
|
||||
results.push(created[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return success(results);
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const key = searchParams.get('key');
|
||||
|
||||
if (!key) {
|
||||
return badRequest('缺少 key 参数');
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(siteConfig)
|
||||
.where(eq(siteConfig.key, key))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
return notFound('配置不存在');
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(siteConfig)
|
||||
.where(eq(siteConfig.key, key));
|
||||
|
||||
return success({ success: true });
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
jest.mock('@/db', () => ({
|
||||
db: {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/permissions', () => ({
|
||||
hasPermission: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/check-permission', () => ({
|
||||
checkIsAdmin: jest.fn(),
|
||||
getAdminUserId: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/audit', () => ({
|
||||
createAuditLog: jest.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
const { db } = require('@/db');
|
||||
const { auth } = require('@/lib/auth');
|
||||
const { hasPermission } = require('@/lib/auth/permissions');
|
||||
const { checkIsAdmin: mockCheckIsAdmin, getAdminUserId: mockGetAdminUserId } = require('@/lib/auth/check-permission');
|
||||
|
||||
describe('GET /api/admin/content/[id]', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const { GET } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123');
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await GET(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const { GET } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123');
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await GET(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 404 if content not found', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
db.limit.mockResolvedValue([]);
|
||||
|
||||
const { GET } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123');
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await GET(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe('内容不存在');
|
||||
});
|
||||
|
||||
it('should return content if found', async () => {
|
||||
const mockContent = {
|
||||
id: '123',
|
||||
title: 'Test Content',
|
||||
status: 'published',
|
||||
};
|
||||
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
db.limit.mockResolvedValue([mockContent]);
|
||||
db.orderBy.mockResolvedValue([]);
|
||||
|
||||
const { GET } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123');
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await GET(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.title).toBe('Test Content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/admin/content/[id]', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const { PUT } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ title: 'Updated' }),
|
||||
});
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await PUT(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const { PUT } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ title: 'Updated' }),
|
||||
});
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await PUT(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/admin/content/[id]', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const { DELETE } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await DELETE(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const { DELETE } = require('./route');
|
||||
const request = new NextRequest('http://localhost/api/admin/content/123', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const params = Promise.resolve({ id: '123' });
|
||||
|
||||
const response = await DELETE(request, { params });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { content, contentVersions } from '@/db/schema';
|
||||
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { forbidden, notFound, success, handleApiError } from '@/lib/api-response';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const item = await db
|
||||
.select()
|
||||
.from(content)
|
||||
.where(eq(content.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (item.length === 0) {
|
||||
return notFound('内容不存在');
|
||||
}
|
||||
|
||||
const versions = await db
|
||||
.select()
|
||||
.from(contentVersions)
|
||||
.where(eq(contentVersions.contentId, id))
|
||||
.orderBy(contentVersions.version);
|
||||
|
||||
return success({
|
||||
...item[0],
|
||||
versions,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
const existingContent = await db
|
||||
.select()
|
||||
.from(content)
|
||||
.where(eq(content.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (existingContent.length === 0) {
|
||||
return notFound('内容不存在');
|
||||
}
|
||||
|
||||
const current = existingContent[0]!;
|
||||
const now = new Date();
|
||||
|
||||
const maxVersion = await db
|
||||
.select({ max: contentVersions.version })
|
||||
.from(contentVersions)
|
||||
.where(eq(contentVersions.contentId, id));
|
||||
|
||||
const nextVersion = (maxVersion[0]?.max || 0) + 1;
|
||||
|
||||
await db.insert(contentVersions).values({
|
||||
id: nanoid(),
|
||||
contentId: id,
|
||||
version: nextVersion,
|
||||
title: current.title,
|
||||
content: current.content,
|
||||
changes: {
|
||||
from: {
|
||||
title: current.title,
|
||||
content: current.content,
|
||||
excerpt: current.excerpt,
|
||||
status: current.status,
|
||||
},
|
||||
to: body,
|
||||
},
|
||||
changedBy: userId,
|
||||
changedAt: now,
|
||||
});
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (body.title) updateData.title = body.title;
|
||||
if (body.slug) updateData.slug = body.slug;
|
||||
if (body.excerpt !== undefined) updateData.excerpt = body.excerpt;
|
||||
if (body.contentBody !== undefined) updateData.content = body.contentBody;
|
||||
if (body.coverImage !== undefined) updateData.coverImage = body.coverImage;
|
||||
if (body.category !== undefined) updateData.category = body.category;
|
||||
if (body.tags !== undefined) updateData.tags = body.tags;
|
||||
if (body.metadata !== undefined) updateData.metadata = body.metadata;
|
||||
|
||||
if (body.status) {
|
||||
updateData.status = body.status;
|
||||
if (body.status === 'published' && current.status !== 'published') {
|
||||
updateData.publishedAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await db
|
||||
.update(content)
|
||||
.set(updateData)
|
||||
.where(eq(content.id, id))
|
||||
.returning();
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
action: 'update',
|
||||
resourceType: 'content',
|
||||
resourceId: id,
|
||||
details: {
|
||||
changes: updateData,
|
||||
},
|
||||
});
|
||||
|
||||
return success(updated[0]);
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const existingContent = await db
|
||||
.select()
|
||||
.from(content)
|
||||
.where(eq(content.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (existingContent.length === 0) {
|
||||
return notFound('内容不存在');
|
||||
}
|
||||
|
||||
await db.delete(contentVersions).where(eq(contentVersions.contentId, id));
|
||||
await db.delete(content).where(eq(content.id, id));
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
action: 'delete',
|
||||
resourceType: 'content',
|
||||
resourceId: id,
|
||||
details: {
|
||||
title: existingContent[0]!.title,
|
||||
},
|
||||
});
|
||||
|
||||
return success({ success: true });
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import { describe, it, expect, jest, beforeAll, beforeEach } from '@jest/globals';
|
||||
import { NextRequest } from 'next/server';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const mockAuth = jest.fn();
|
||||
const mockHasPermission = jest.fn();
|
||||
const mockCheckIsAdmin = jest.fn();
|
||||
const mockGetAdminUserId = jest.fn();
|
||||
const mockDbSelect = jest.fn();
|
||||
const mockDbInsert = jest.fn();
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
auth: mockAuth,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/check-permission', () => ({
|
||||
checkIsAdmin: mockCheckIsAdmin,
|
||||
getAdminUserId: mockGetAdminUserId,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/permissions', () => ({
|
||||
hasPermission: mockHasPermission,
|
||||
}));
|
||||
|
||||
jest.mock('@/db', () => ({
|
||||
db: {
|
||||
select: () => ({
|
||||
from: () => ({
|
||||
where: () => ({
|
||||
orderBy: () => ({
|
||||
limit: () => ({
|
||||
offset: mockDbSelect,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: () => ({
|
||||
values: () => ({
|
||||
returning: mockDbInsert,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('drizzle-orm', () => ({
|
||||
eq: jest.fn(),
|
||||
desc: jest.fn(),
|
||||
and: jest.fn(),
|
||||
like: jest.fn(),
|
||||
sql: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: () => 'test-id-123',
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/audit', () => ({
|
||||
createAuditLog: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/db/schema', () => ({
|
||||
content: {},
|
||||
}));
|
||||
|
||||
import { GET, POST } from './route';
|
||||
|
||||
describe('/api/admin/content', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET', () => {
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/content');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 when user lacks permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false, userId: '1' });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/content');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return content list when authorized', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
mockDbSelect.mockResolvedValueOnce([]);
|
||||
mockDbSelect.mockResolvedValueOnce([{ count: 0 }]);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/content');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.items).toEqual([]);
|
||||
expect(data.pagination).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST', () => {
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/content', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'news', title: 'Test', slug: 'test' }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 400 when missing required fields', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
mockGetAdminUserId.mockResolvedValueOnce('1');
|
||||
mockDbSelect.mockResolvedValueOnce([]);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/content', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'news' }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe('缺少必要字段');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,296 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { content } from '@/db/schema';
|
||||
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { forbidden, badRequest, success, handleApiError, validationError } from '@/lib/api-response';
|
||||
import { eq, desc, and, like, sql } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /api/admin/content:
|
||||
* get:
|
||||
* tags:
|
||||
* - Admin
|
||||
* - Content
|
||||
* summary: 获取内容列表
|
||||
* description: 管理员获取内容列表,支持分页、筛选和搜索
|
||||
* operationId: getAdminContent
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - name: type
|
||||
* in: query
|
||||
* description: 内容类型
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [news, product, service, case]
|
||||
* - name: status
|
||||
* in: query
|
||||
* description: 内容状态
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [draft, published, archived]
|
||||
* - name: search
|
||||
* in: query
|
||||
* description: 搜索关键词
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: page
|
||||
* in: query
|
||||
* description: 页码
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* - name: limit
|
||||
* in: query
|
||||
* description: 每页数量
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 20
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取内容列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Content'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* limit:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const type = searchParams.get('type');
|
||||
const status = searchParams.get('status');
|
||||
const search = searchParams.get('search');
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '20');
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (type) {
|
||||
conditions.push(eq(content.type, type as 'news' | 'product' | 'service' | 'case'));
|
||||
}
|
||||
|
||||
if (status) {
|
||||
conditions.push(eq(content.status, status as 'draft' | 'published' | 'archived'));
|
||||
}
|
||||
|
||||
if (search) {
|
||||
conditions.push(like(content.title, `%${search}%`));
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(content)
|
||||
.where(whereClause)
|
||||
.orderBy(desc(content.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(content)
|
||||
.where(whereClause),
|
||||
]);
|
||||
|
||||
const total = countResult[0]?.count || 0;
|
||||
|
||||
return success({
|
||||
items,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /api/admin/content:
|
||||
* post:
|
||||
* tags:
|
||||
* - Admin
|
||||
* - Content
|
||||
* summary: 创建新内容
|
||||
* description: 管理员创建新的内容(新闻、产品、服务、案例)
|
||||
* operationId: createContent
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - type
|
||||
* - title
|
||||
* - slug
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [news, product, service, case]
|
||||
* description: 内容类型
|
||||
* title:
|
||||
* type: string
|
||||
* description: 标题
|
||||
* slug:
|
||||
* type: string
|
||||
* description: URL别名
|
||||
* excerpt:
|
||||
* type: string
|
||||
* description: 摘要
|
||||
* contentBody:
|
||||
* type: string
|
||||
* description: 内容正文
|
||||
* coverImage:
|
||||
* type: string
|
||||
* description: 封面图片URL
|
||||
* category:
|
||||
* type: string
|
||||
* description: 分类
|
||||
* tags:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 标签列表
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [draft, published, archived]
|
||||
* default: draft
|
||||
* description: 状态
|
||||
* metadata:
|
||||
* type: object
|
||||
* description: 元数据
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 内容创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Content'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { type, title, slug, excerpt, contentBody, coverImage, category, tags, status: contentStatus, metadata } = body;
|
||||
|
||||
if (!type || !title || !slug) {
|
||||
return validationError('缺少必要字段', { required: ['type', 'title', 'slug'] });
|
||||
}
|
||||
|
||||
const existingContent = await db
|
||||
.select()
|
||||
.from(content)
|
||||
.where(eq(content.slug, slug))
|
||||
.limit(1);
|
||||
|
||||
if (existingContent.length > 0) {
|
||||
return badRequest('Slug 已存在');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const newContent = await db
|
||||
.insert(content)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
type,
|
||||
title,
|
||||
slug,
|
||||
excerpt: excerpt || null,
|
||||
content: contentBody || '',
|
||||
coverImage: coverImage || null,
|
||||
category: category || null,
|
||||
tags: tags || [],
|
||||
status: contentStatus || 'draft',
|
||||
publishedAt: contentStatus === 'published' ? now : null,
|
||||
authorId: userId,
|
||||
metadata: metadata || null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.returning();
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
action: 'create',
|
||||
resourceType: 'content',
|
||||
resourceId: newContent[0]!.id,
|
||||
details: {
|
||||
type,
|
||||
title,
|
||||
status: contentStatus || 'draft',
|
||||
},
|
||||
});
|
||||
|
||||
return success(newContent[0], 201);
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { SecurityLogger } from '@/lib/security/logger';
|
||||
|
||||
const securityLogger = new SecurityLogger();
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const logs = securityLogger.getRecentLogs(100);
|
||||
const stats = securityLogger.getStats();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
logs,
|
||||
stats,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching security data:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch security data'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { POST, DELETE } from './route';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/permissions', () => ({
|
||||
hasPermission: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/check-permission', () => ({
|
||||
checkIsAdmin: jest.fn(),
|
||||
getAdminUserId: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/audit', () => ({
|
||||
createAuditLog: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/upload', () => ({
|
||||
uploadFile: jest.fn().mockResolvedValue({
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
type: 'image',
|
||||
size: 1024,
|
||||
url: 'https://example.com/test.jpg',
|
||||
}),
|
||||
deleteFile: jest.fn(),
|
||||
}));
|
||||
|
||||
const { checkIsAdmin: mockCheckIsAdmin, getAdminUserId: mockGetAdminUserId } = require('@/lib/auth/check-permission');
|
||||
|
||||
describe('/api/admin/upload', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('POST', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', new File(['test'], 'test.jpg', { type: 'image/jpeg' }));
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/upload', {
|
||||
method: 'POST',
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 400 if no file', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
mockGetAdminUserId.mockResolvedValueOnce('1');
|
||||
|
||||
const request = {
|
||||
formData: jest.fn().mockResolvedValue(new FormData()),
|
||||
} as any;
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe('未找到文件');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
mockGetAdminUserId.mockResolvedValueOnce(null);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/upload?url=test.jpg', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const response = await DELETE(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
import { uploadFile, deleteFile } from '@/lib/upload';
|
||||
import { forbidden, badRequest, notFound, success, handleApiError } from '@/lib/api-response';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
const type = (formData.get('type') as 'image' | 'document') || 'image';
|
||||
|
||||
if (!file) {
|
||||
return badRequest('未找到文件');
|
||||
}
|
||||
|
||||
const result = await uploadFile(file, {
|
||||
type,
|
||||
userId,
|
||||
});
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
action: 'upload',
|
||||
resourceType: 'file',
|
||||
resourceId: result.id,
|
||||
details: {
|
||||
fileName: result.name,
|
||||
fileType: result.type,
|
||||
fileSize: result.size,
|
||||
url: result.url,
|
||||
},
|
||||
});
|
||||
|
||||
return success({
|
||||
success: true,
|
||||
file: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
return badRequest(error.message);
|
||||
}
|
||||
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
const userId = await getAdminUserId();
|
||||
|
||||
if (!isAdmin || !userId) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fileUrl = searchParams.get('url');
|
||||
|
||||
if (!fileUrl) {
|
||||
return badRequest('缺少文件 URL');
|
||||
}
|
||||
|
||||
const result = await deleteFile(fileUrl);
|
||||
|
||||
if (!result) {
|
||||
return notFound('文件不存在或删除失败');
|
||||
}
|
||||
|
||||
return success({ success: true });
|
||||
} catch (error) {
|
||||
console.error('文件删除失败:', error);
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { GET, PUT, DELETE } from './route';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/permissions', () => ({
|
||||
hasPermission: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/check-permission', () => ({
|
||||
checkIsAdmin: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/db', () => ({
|
||||
db: {
|
||||
select: jest.fn().mockReturnValue({
|
||||
from: jest.fn().mockReturnValue({
|
||||
where: jest.fn().mockReturnValue({
|
||||
limit: jest.fn().mockResolvedValue([{
|
||||
id: 'test-user-id',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
isAdmin: true,
|
||||
}]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
update: jest.fn().mockReturnValue({
|
||||
set: jest.fn().mockReturnValue({
|
||||
where: jest.fn().mockReturnValue({
|
||||
returning: jest.fn().mockResolvedValue([{
|
||||
id: 'test-user-id',
|
||||
email: 'updated@example.com',
|
||||
name: 'Updated User',
|
||||
}]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
delete: jest.fn().mockReturnValue({
|
||||
where: jest.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const { checkIsAdmin: mockCheckIsAdmin } = require('@/lib/auth/check-permission');
|
||||
|
||||
describe('/api/admin/users/[id]', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users/test-id');
|
||||
const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 if no permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users/test-id');
|
||||
const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return user if authenticated and has permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users/test-id');
|
||||
const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.user).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users/test-id', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: 'Updated User' }),
|
||||
});
|
||||
const response = await PUT(request, { params: Promise.resolve({ id: 'test-id' }) });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE', () => {
|
||||
it('should return 401 if not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users/test-id', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const response = await DELETE(request, { params: Promise.resolve({ id: 'test-id' }) });
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { users } from '@/db/schema';
|
||||
import { checkIsAdmin } from '@/lib/auth/check-permission';
|
||||
import { forbidden, notFound, success, handleApiError } from '@/lib/api-response';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const user = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
name: users.name,
|
||||
isAdmin: users.isAdmin,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (user.length === 0) {
|
||||
return notFound('用户不存在');
|
||||
}
|
||||
|
||||
return success({ user: user[0] });
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const body = await request.json();
|
||||
const { email, name, password } = body;
|
||||
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (existingUser.length === 0) {
|
||||
return notFound('用户不存在');
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (email) updateData.email = email;
|
||||
if (name) updateData.name = name;
|
||||
if (password) {
|
||||
updateData.passwordHash = await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
const updated = await db
|
||||
.update(users)
|
||||
.set(updateData)
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
|
||||
return success({
|
||||
user: {
|
||||
id: updated[0]!.id,
|
||||
email: updated[0]!.email,
|
||||
name: updated[0]!.name,
|
||||
isAdmin: updated[0]!.isAdmin,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
await db
|
||||
.delete(users)
|
||||
.where(eq(users.id, id));
|
||||
|
||||
return success({ success: true });
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, it, expect, jest, beforeAll, beforeEach } from '@jest/globals';
|
||||
import { NextRequest } from 'next/server';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const mockAuth = jest.fn();
|
||||
const mockHasPermission = jest.fn();
|
||||
const mockCheckIsAdmin = jest.fn();
|
||||
const mockDbSelect = jest.fn();
|
||||
const mockDbInsert = jest.fn();
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
auth: mockAuth,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/check-permission', () => ({
|
||||
checkIsAdmin: mockCheckIsAdmin,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/auth/permissions', () => ({
|
||||
hasPermission: mockHasPermission,
|
||||
}));
|
||||
|
||||
jest.mock('@/db', () => ({
|
||||
db: {
|
||||
select: () => ({
|
||||
from: () => ({
|
||||
where: () => ({
|
||||
limit: mockDbSelect,
|
||||
}),
|
||||
orderBy: () => mockDbSelect(),
|
||||
}),
|
||||
}),
|
||||
insert: () => ({
|
||||
values: () => ({
|
||||
returning: mockDbInsert,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('drizzle-orm', () => ({
|
||||
eq: jest.fn(),
|
||||
desc: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: () => 'test-id-123',
|
||||
}));
|
||||
|
||||
jest.mock('bcryptjs', () => ({
|
||||
hash: jest.fn().mockResolvedValue('hashed-password'),
|
||||
}));
|
||||
|
||||
jest.mock('@/db/schema', () => ({
|
||||
users: {},
|
||||
}));
|
||||
|
||||
import { GET } from './route';
|
||||
|
||||
describe('/api/admin/users', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET', () => {
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return 403 when user lacks permission', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false, userId: '1' });
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe('无权限执行此操作');
|
||||
});
|
||||
|
||||
it('should return users list when authorized', async () => {
|
||||
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
|
||||
mockDbSelect.mockResolvedValueOnce([
|
||||
{ id: '1', email: 'admin@example.com', name: 'Admin', isAdmin: true },
|
||||
]);
|
||||
|
||||
const request = new NextRequest('http://localhost/api/admin/users');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.users).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { users } from '@/db/schema';
|
||||
import { checkIsAdmin } from '@/lib/auth/check-permission';
|
||||
import { forbidden, success, handleApiError } from '@/lib/api-response';
|
||||
import { desc } from 'drizzle-orm';
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const { isAdmin } = await checkIsAdmin();
|
||||
|
||||
if (!isAdmin) {
|
||||
return forbidden();
|
||||
}
|
||||
|
||||
const allUsers = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
name: users.name,
|
||||
isAdmin: users.isAdmin,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
})
|
||||
.from(users)
|
||||
.orderBy(desc(users.createdAt));
|
||||
|
||||
return success({ users: allUsers });
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user