feat: 修复测试套件问题并添加Woodpecker CI配置

- 修复API测试认证问题:创建全局认证设置,更新Playwright配置
- 优化回归测试稳定性:增加超时时间到15秒,修复定位器
- 创建Woodpecker CI工作流:CI、部署和质量门禁配置
- 添加Jest配置和测试脚本
- 移除登录页面的默认账号密码显示(安全问题修复)
This commit is contained in:
张翔
2026-03-09 10:26:02 +08:00
parent 96c96fe75d
commit 6d92024b63
68 changed files with 5584 additions and 167 deletions
+203
View File
@@ -0,0 +1,203 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { siteConfig } from '@/db/schema';
import { auth } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/permissions';
import { eq, and } from 'drizzle-orm';
import { nanoid } from 'nanoid';
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'config', 'read')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
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 NextResponse.json({ error: '配置不存在' }, { status: 404 });
}
return NextResponse.json(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);
const groupedConfigs = configs.reduce((acc, config) => {
const cat = config.category;
if (!acc[cat]) {
acc[cat] = [];
}
acc[cat].push(config);
return acc;
}, {} as Record<string, typeof configs>);
return NextResponse.json({
configs: groupedConfigs,
flat: configs,
});
} catch (error) {
console.error('获取配置失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'config', 'update')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const body = await request.json();
const { key, value, category, description } = body;
if (!key || !value || !category) {
return NextResponse.json({ error: '缺少必要字段' }, { status: 400 });
}
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: session.user.id,
})
.where(eq(siteConfig.key, key))
.returning();
return NextResponse.json(updated[0]);
}
const newConfig = await db
.insert(siteConfig)
.values({
id: nanoid(),
key,
value,
category,
description: description || null,
updatedAt: now,
updatedBy: session.user.id,
})
.returning();
return NextResponse.json(newConfig[0], { status: 201 });
} catch (error) {
console.error('创建/更新配置失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function PUT(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'config', 'update')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const body = await request.json();
const { configs } = body as { configs: Array<{ key: string; value: unknown; description?: string }> };
if (!Array.isArray(configs)) {
return NextResponse.json({ error: '无效的数据格式' }, { status: 400 });
}
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: session.user.id,
})
.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: session.user.id,
})
.returning();
results.push(created[0]);
}
}
return NextResponse.json({ success: true, updated: results.length });
} catch (error) {
console.error('批量更新配置失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
+204
View File
@@ -0,0 +1,204 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { content, contentVersions } from '@/db/schema';
import { auth } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/permissions';
import { createAuditLog } from '@/lib/audit';
import { eq } from 'drizzle-orm';
import { nanoid } from 'nanoid';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'read')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const { id } = await params;
const item = await db
.select()
.from(content)
.where(eq(content.id, id))
.limit(1);
if (item.length === 0) {
return NextResponse.json({ error: '内容不存在' }, { status: 404 });
}
const versions = await db
.select()
.from(contentVersions)
.where(eq(contentVersions.contentId, id))
.orderBy(contentVersions.version);
return NextResponse.json({
...item[0],
versions,
});
} catch (error) {
console.error('获取内容详情失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'update')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
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 NextResponse.json({ error: '内容不存在' }, { status: 404 });
}
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: session.user.id,
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: session.user.id,
action: 'update',
resourceType: 'content',
resourceId: id,
details: {
changes: updateData,
},
});
return NextResponse.json(updated[0]);
} catch (error) {
console.error('更新内容失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'delete')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const { id } = await params;
const existingContent = await db
.select()
.from(content)
.where(eq(content.id, id))
.limit(1);
if (existingContent.length === 0) {
return NextResponse.json({ error: '内容不存在' }, { status: 404 });
}
await db.delete(contentVersions).where(eq(contentVersions.contentId, id));
await db.delete(content).where(eq(content.id, id));
await createAuditLog({
userId: session.user.id,
action: 'delete',
resourceType: 'content',
resourceId: id,
details: {
title: existingContent[0]!.title,
},
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('删除内容失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
+149
View File
@@ -0,0 +1,149 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { content } from '@/db/schema';
import { auth } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/permissions';
import { createAuditLog } from '@/lib/audit';
import { eq, desc, and, like, sql } from 'drizzle-orm';
import { nanoid } from 'nanoid';
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'read')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
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 NextResponse.json({
items,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('获取内容列表失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'create')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const body = await request.json();
const { type, title, slug, excerpt, contentBody, coverImage, category, tags, status: contentStatus, metadata } = body;
if (!type || !title || !slug) {
return NextResponse.json({ error: '缺少必要字段' }, { status: 400 });
}
const existingContent = await db
.select()
.from(content)
.where(eq(content.slug, slug))
.limit(1);
if (existingContent.length > 0) {
return NextResponse.json({ error: 'Slug 已存在' }, { status: 400 });
}
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: session.user.id,
metadata: metadata || null,
createdAt: now,
updatedAt: now,
})
.returning();
await createAuditLog({
userId: session.user.id,
action: 'create',
resourceType: 'content',
resourceId: newContent[0]!.id,
details: {
type,
title,
status: contentStatus || 'draft',
},
});
return NextResponse.json(newContent[0], { status: 201 });
} catch (error) {
console.error('创建内容失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
+94
View File
@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/permissions';
import { createAuditLog } from '@/lib/audit';
import { uploadFile, deleteFile } from '@/lib/upload';
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'create')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
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 NextResponse.json({ error: '未找到文件' }, { status: 400 });
}
const result = await uploadFile(file, {
type,
userId: session.user.id,
});
await createAuditLog({
userId: session.user.id,
action: 'upload',
resourceType: 'file',
resourceId: result.id,
details: {
fileName: result.name,
fileType: result.type,
fileSize: result.size,
url: result.url,
},
});
return NextResponse.json({
success: true,
file: result,
});
} catch (error) {
console.error('文件上传失败:', error);
if (error instanceof Error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'content', 'delete')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const fileUrl = searchParams.get('url');
if (!fileUrl) {
return NextResponse.json({ error: '缺少文件 URL' }, { status: 400 });
}
const success = await deleteFile(fileUrl);
if (!success) {
return NextResponse.json({ error: '文件不存在或删除失败' }, { status: 404 });
}
return NextResponse.json({ success: true });
} catch (error) {
console.error('文件删除失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
+155
View File
@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { users } from '@/db/schema';
import { auth } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/permissions';
import { eq } from 'drizzle-orm';
import bcrypt from 'bcryptjs';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'users', 'read')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const { id } = await params;
const user = await db
.select({
id: users.id,
email: users.email,
name: users.name,
role: users.role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.id, id))
.limit(1);
if (user.length === 0) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
}
return NextResponse.json({ user: user[0] });
} catch (error) {
console.error('获取用户详情失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'users', 'update')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const { id } = await params;
const body = await request.json();
const { email, name, password, role } = body;
const existingUser = await db
.select()
.from(users)
.where(eq(users.id, id))
.limit(1);
if (existingUser.length === 0) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
}
const updateData: Record<string, any> = {
updatedAt: new Date(),
};
if (email) updateData.email = email;
if (name) updateData.name = name;
if (role) updateData.role = role;
if (password) {
updateData.passwordHash = await bcrypt.hash(password, 10);
}
const updated = await db
.update(users)
.set(updateData)
.where(eq(users.id, id))
.returning();
return NextResponse.json({
user: {
id: updated[0]!.id,
email: updated[0]!.email,
name: updated[0]!.name,
role: updated[0]!.role,
createdAt: updated[0]!.createdAt,
}
});
} catch (error) {
console.error('更新用户失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'users', 'delete')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const { id } = await params;
if (session.user.id === id) {
return NextResponse.json({ error: '不能删除自己的账号' }, { status: 400 });
}
const existingUser = await db
.select()
.from(users)
.where(eq(users.id, id))
.limit(1);
if (existingUser.length === 0) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
}
await db.delete(users).where(eq(users.id, id));
return NextResponse.json({ success: true });
} catch (error) {
console.error('删除用户失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
+102
View File
@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { users } from '@/db/schema';
import { auth } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/permissions';
import { eq } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import bcrypt from 'bcryptjs';
export async function GET(_request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'users', 'read')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const allUsers = await db
.select({
id: users.id,
email: users.email,
name: users.name,
role: users.role,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.orderBy(users.createdAt);
return NextResponse.json({ users: allUsers });
} catch (error) {
console.error('获取用户列表失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
if (!hasPermission(userRole, 'users', 'create')) {
return NextResponse.json({ error: '无权限' }, { status: 403 });
}
const body = await request.json();
const { email, name, password, role } = body;
if (!email || !name || !password || !role) {
return NextResponse.json({ error: '缺少必填字段' }, { status: 400 });
}
const existingUser = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (existingUser.length > 0) {
return NextResponse.json({ error: '邮箱已被使用' }, { status: 400 });
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = await db
.insert(users)
.values({
id: nanoid(),
email,
name,
passwordHash: hashedPassword,
role,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
return NextResponse.json({
user: {
id: newUser[0]!.id,
email: newUser[0]!.email,
name: newUser[0]!.name,
role: newUser[0]!.role,
createdAt: newUser[0]!.createdAt,
}
});
} catch (error) {
console.error('创建用户失败:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
+3 -4
View File
@@ -1,6 +1,5 @@
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
import { handlers } from '@/lib/auth';
const handler = NextAuth(authOptions);
export const dynamic = 'force-dynamic';
export { handler as GET, handler as POST };
export const { GET, POST } = handlers;