feat: implement frontend-backend configuration linkage

- Create public config API for frontend consumption
- Add configuration fetching to homepage
- Implement module show/hide logic based on config
- Add support for Services items filtering
- Add support for Products featured products and pricing display
- Add support for News display count, categories, and sort order
- Fix table name from 'configs' to 'siteConfig' in API route
- Update type definitions for proper TypeScript support
This commit is contained in:
张翔
2026-03-13 13:11:20 +08:00
parent f93f802427
commit 4fdfc2d8b4
100 changed files with 894 additions and 316 deletions
+20 -22
View File
@@ -9,6 +9,10 @@ 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({
@@ -18,7 +22,7 @@ jest.mock('@/db', () => ({
id: 'test-user-id',
email: 'test@example.com',
name: 'Test User',
role: 'admin',
isAdmin: true,
}]),
}),
}),
@@ -40,6 +44,8 @@ jest.mock('@/db', () => ({
},
}));
const { checkIsAdmin: mockCheckIsAdmin } = require('@/lib/auth/check-permission');
describe('/api/admin/users/[id]', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -47,35 +53,29 @@ describe('/api/admin/users/[id]', () => {
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(401);
expect(data.error).toBe('未授权');
expect(response.status).toBe(403);
expect(data.error).toBe('无权限执行此操作');
});
it('should return 403 if no permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'viewer' } });
hasPermission.mockReturnValue(false);
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('无权限');
expect(data.error).toBe('无权限执行此操作');
});
it('should return user if authenticated and has permission', async () => {
const { auth } = require('@/lib/auth');
const { hasPermission } = require('@/lib/auth/permissions');
auth.mockResolvedValue({ user: { role: 'admin' } });
hasPermission.mockReturnValue(true);
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' }) });
@@ -88,8 +88,7 @@ describe('/api/admin/users/[id]', () => {
describe('PUT', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
const request = new NextRequest('http://localhost/api/admin/users/test-id', {
method: 'PUT',
@@ -98,15 +97,14 @@ describe('/api/admin/users/[id]', () => {
const response = await PUT(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
expect(response.status).toBe(403);
expect(data.error).toBe('无权限执行此操作');
});
});
describe('DELETE', () => {
it('should return 401 if not authenticated', async () => {
const { auth } = require('@/lib/auth');
auth.mockResolvedValue(null);
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false });
const request = new NextRequest('http://localhost/api/admin/users/test-id', {
method: 'DELETE',
@@ -114,8 +112,8 @@ describe('/api/admin/users/[id]', () => {
const response = await DELETE(request, { params: Promise.resolve({ id: 'test-id' }) });
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
expect(response.status).toBe(403);
expect(data.error).toBe('无权限执行此操作');
});
});
});
+3 -15
View File
@@ -11,7 +11,7 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { isAdmin, userId } = await checkIsAdmin();
const { isAdmin } = await checkIsAdmin();
if (!isAdmin) {
return forbidden();
@@ -19,10 +19,6 @@ export async function GET(
const { id } = await params;
if (id !== userId) {
return forbidden('只能查看自己的信息');
}
const user = await db
.select({
id: users.id,
@@ -51,7 +47,7 @@ export async function PUT(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { isAdmin, userId } = await checkIsAdmin();
const { isAdmin } = await checkIsAdmin();
if (!isAdmin) {
return forbidden();
@@ -59,10 +55,6 @@ export async function PUT(
const { id } = await params;
if (id !== userId) {
return forbidden('只能修改自己的信息');
}
const body = await request.json();
const { email, name, password } = body;
@@ -110,7 +102,7 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { isAdmin, userId } = await checkIsAdmin();
const { isAdmin } = await checkIsAdmin();
if (!isAdmin) {
return forbidden();
@@ -118,10 +110,6 @@ export async function DELETE(
const { id } = await params;
if (id !== userId) {
return forbidden('不能删除其他用户');
}
await db
.delete(users)
.where(eq(users.id, id));
+23 -56
View File
@@ -4,6 +4,7 @@ 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();
@@ -11,6 +12,10 @@ jest.mock('@/lib/auth', () => ({
auth: mockAuth,
}));
jest.mock('@/lib/auth/check-permission', () => ({
checkIsAdmin: mockCheckIsAdmin,
}));
jest.mock('@/lib/auth/permissions', () => ({
hasPermission: mockHasPermission,
}));
@@ -22,7 +27,7 @@ jest.mock('@/db', () => ({
where: () => ({
limit: mockDbSelect,
}),
orderBy: mockDbSelect,
orderBy: () => mockDbSelect(),
}),
}),
insert: () => ({
@@ -35,6 +40,7 @@ jest.mock('@/db', () => ({
jest.mock('drizzle-orm', () => ({
eq: jest.fn(),
desc: jest.fn(),
}));
jest.mock('nanoid', () => ({
@@ -49,7 +55,7 @@ jest.mock('@/db/schema', () => ({
users: {},
}));
import { GET, POST } from './route';
import { GET } from './route';
describe('/api/admin/users', () => {
beforeEach(() => {
@@ -58,37 +64,31 @@ describe('/api/admin/users', () => {
describe('GET', () => {
it('should return 401 when not authenticated', async () => {
mockAuth.mockResolvedValueOnce(null);
const request = new NextRequest('http://localhost/api/admin/users');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 403 when user lacks permission', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'viewer' },
});
mockHasPermission.mockReturnValueOnce(false);
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('无权限');
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 () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'admin' },
});
mockHasPermission.mockReturnValueOnce(true);
mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' });
mockDbSelect.mockResolvedValueOnce([
{ id: '1', email: 'admin@example.com', name: 'Admin', role: 'admin' },
{ id: '1', email: 'admin@example.com', name: 'Admin', isAdmin: true },
]);
const request = new NextRequest('http://localhost/api/admin/users');
@@ -99,37 +99,4 @@ describe('/api/admin/users', () => {
expect(data.users).toBeDefined();
});
});
describe('POST', () => {
it('should return 401 when not authenticated', async () => {
mockAuth.mockResolvedValueOnce(null);
const request = new NextRequest('http://localhost/api/admin/users', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com', name: 'Test', password: 'password', role: 'viewer' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.error).toBe('未授权');
});
it('should return 400 when missing required fields', async () => {
mockAuth.mockResolvedValueOnce({
user: { id: '1', role: 'admin' },
});
mockHasPermission.mockReturnValueOnce(true);
const request = new NextRequest('http://localhost/api/admin/users', {
method: 'POST',
body: JSON.stringify({ email: 'test@example.com' }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe('缺少必填字段');
});
});
});