dev #5
@@ -17,15 +17,30 @@ export class AdminUserPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createUser(data: UserData) {
|
async createUser(data: UserData) {
|
||||||
await this.goto();
|
console.log('开始创建用户:', data.email);
|
||||||
|
|
||||||
|
await this.goto();
|
||||||
await this.page.waitForLoadState('domcontentloaded');
|
await this.page.waitForLoadState('domcontentloaded');
|
||||||
await this.page.waitForTimeout(1000);
|
|
||||||
|
console.log('页面加载完成,准备点击添加用户按钮');
|
||||||
|
|
||||||
const addButton = this.page.locator('button:has-text("添加用户")');
|
const addButton = this.page.locator('button:has-text("添加用户")');
|
||||||
await addButton.waitFor({ timeout: 10000, state: 'visible' });
|
await addButton.waitFor({ timeout: 10000, state: 'visible' });
|
||||||
await addButton.click();
|
await addButton.click();
|
||||||
|
|
||||||
|
console.log('已点击添加用户按钮,等待模态框打开');
|
||||||
|
|
||||||
|
await this.page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await this.page.waitForSelector('.fixed.inset-0.bg-black.bg-opacity-50', {
|
||||||
|
timeout: 5000,
|
||||||
|
state: 'visible'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('模态框已打开,等待表单加载');
|
||||||
|
|
||||||
|
await this.page.waitForTimeout(300);
|
||||||
|
|
||||||
await this.page.waitForSelector('input[name="email"]', { timeout: 5000, state: 'visible' });
|
await this.page.waitForSelector('input[name="email"]', { timeout: 5000, state: 'visible' });
|
||||||
await this.page.fill('input[name="email"]', data.email);
|
await this.page.fill('input[name="email"]', data.email);
|
||||||
await this.page.fill('input[name="password"]', data.password);
|
await this.page.fill('input[name="password"]', data.password);
|
||||||
@@ -38,12 +53,40 @@ export class AdminUserPage {
|
|||||||
await this.page.selectOption('select[name="role"]', data.role);
|
await this.page.selectOption('select[name="role"]', data.role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('表单填写完成,准备提交');
|
||||||
|
|
||||||
await this.page.click('button:has-text("创建")');
|
await this.page.click('button:has-text("创建")');
|
||||||
|
|
||||||
|
console.log('用户创建请求已提交');
|
||||||
}
|
}
|
||||||
|
|
||||||
async expectUserInList(email: string) {
|
async expectUserInList(email: string) {
|
||||||
|
console.log(`检查用户是否在列表中: ${email}`);
|
||||||
|
|
||||||
await this.goto();
|
await this.goto();
|
||||||
const row = this.page.locator(`tr:has-text("${email}")`);
|
await this.page.waitForLoadState('domcontentloaded');
|
||||||
await expect(row).toBeVisible();
|
await this.page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
let row = this.page.locator(`tr:has-text("${email}")`);
|
||||||
|
let isVisible = await row.count() > 0;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
console.log('用户不在列表中,尝试刷新页面');
|
||||||
|
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
await this.page.waitForSelector('table tbody tr', { timeout: 5000 });
|
||||||
|
await this.page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
row = this.page.locator(`tr:has-text("${email}")`);
|
||||||
|
isVisible = await row.count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
const allRows = await this.page.locator('table tbody tr').allTextContents();
|
||||||
|
console.log('当前列表中的用户:');
|
||||||
|
allRows.forEach((text, i) => console.log(`行 ${i + 1}: ${text}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(row).toBeVisible({ timeout: 10000 });
|
||||||
|
console.log(`✅ 用户已在列表中: ${email}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ export default function UsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
name="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -271,6 +272,7 @@ export default function UsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">姓名</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">姓名</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
name="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -280,6 +282,7 @@ export default function UsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
name="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -288,6 +291,7 @@ export default function UsersPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">角色</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">角色</label>
|
||||||
<select
|
<select
|
||||||
|
name="role"
|
||||||
value={formData.role}
|
value={formData.role}
|
||||||
onChange={(e) => setFormData({ ...formData, role: e.target.value as any })}
|
onChange={(e) => setFormData({ ...formData, role: e.target.value as any })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -330,6 +334,7 @@ export default function UsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
name="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -339,6 +344,7 @@ export default function UsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">姓名</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">姓名</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
name="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -348,6 +354,7 @@ export default function UsersPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-1">新密码(留空则不修改)</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">新密码(留空则不修改)</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
name="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
@@ -356,6 +363,7 @@ export default function UsersPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">角色</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">角色</label>
|
||||||
<select
|
<select
|
||||||
|
name="role"
|
||||||
value={formData.role}
|
value={formData.role}
|
||||||
onChange={(e) => setFormData({ ...formData, role: e.target.value as any })}
|
onChange={(e) => setFormData({ ...formData, role: e.target.value as any })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#C41E3A]"
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { NextRequest } from 'next/server';
|
|||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { users } from '@/db/schema';
|
import { users } from '@/db/schema';
|
||||||
import { checkIsAdmin } from '@/lib/auth/check-permission';
|
import { checkIsAdmin } from '@/lib/auth/check-permission';
|
||||||
import { forbidden, success, handleApiError } from '@/lib/api-response';
|
import { forbidden, success, handleApiError, badRequest } from '@/lib/api-response';
|
||||||
import { desc } from 'drizzle-orm';
|
import { desc } from 'drizzle-orm';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export async function GET(_request: NextRequest) {
|
export async function GET(_request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -31,3 +33,40 @@ export async function GET(_request: NextRequest) {
|
|||||||
return handleApiError(error);
|
return handleApiError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { email, name, password, role } = body;
|
||||||
|
|
||||||
|
if (!email || !name || !password || !role) {
|
||||||
|
return badRequest('缺少必填字段');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
const [newUser] = await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
id: nanoid(),
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
password: hashedPassword,
|
||||||
|
isAdmin: role === 'admin',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return success({ user: newUser });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建用户失败:', error);
|
||||||
|
return handleApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user