feat: implement frontend-backend encrypted communication via AES-256-GCM
参考 novavis-authority 的加解密方案,实现前后端通信的应用层加密: - 重写 src/lib/crypto.ts 使用 Web Crypto API(浏览器兼容),PBKDF2+AES-256-GCM - 新增 src/lib/crypto-server.ts 服务端加解密工具(Node.js crypto) - 新增 src/lib/api-crypto.ts API 路由中间件 withCrypto(),自动解密请求体/加密响应体 - 更新 src/lib/admin-api.ts 自动加密所有请求/解密响应 - 所有 11 个 admin API 路由文件已应用 withCrypto 包装器 - 更新 .env 文件,添加 NEXT_PUBLIC_ENCRYPTION_SECRET 和 ENCRYPTION_SECRET
This commit is contained in:
@@ -42,6 +42,8 @@ import { POST } from './route';
|
||||
|
||||
function createMockRequest(body: Record<string, unknown>): NextRequest {
|
||||
return {
|
||||
headers: new Headers(),
|
||||
url: 'http://localhost:3000/api/admin/items/item-1/workflow',
|
||||
json: async () => body,
|
||||
} as unknown as NextRequest;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
internalError,
|
||||
forbidden,
|
||||
} from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
function parseItem(item: { data: string; [key: string]: unknown }) {
|
||||
return { ...item, data: JSON.parse(item.data as string) };
|
||||
@@ -32,10 +33,10 @@ const ACTION_PERMISSION: Record<WorkflowAction, 'update' | 'publish'> = {
|
||||
archive: 'publish',
|
||||
};
|
||||
|
||||
export async function POST(
|
||||
export const POST = withCrypto(async (
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
) => {
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
@@ -90,4 +91,4 @@ export async function POST(
|
||||
console.error('Workflow error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ function createMockRequest(options: {
|
||||
json?: () => Promise<Record<string, unknown>>;
|
||||
}): NextRequest {
|
||||
return {
|
||||
headers: new Headers(),
|
||||
url: options.url || 'http://localhost/api/admin/items',
|
||||
json: options.json || (async () => ({})),
|
||||
} as unknown as NextRequest;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
validationError,
|
||||
internalError,
|
||||
} from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
function parseItem(item: { data: string; [key: string]: unknown }) {
|
||||
return { ...item, data: JSON.parse(item.data as string) };
|
||||
@@ -46,7 +47,7 @@ async function ensureUniqueSlug(
|
||||
}
|
||||
|
||||
// GET /api/admin/items - 获取内容列表
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const modelCode = searchParams.get('modelCode');
|
||||
|
||||
@@ -91,10 +92,10 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get items error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/items - 创建内容
|
||||
export async function POST(request: NextRequest) {
|
||||
export const POST = withCrypto(async (request: NextRequest) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { modelId, modelCode, title, slug, data, status, sortOrder } = body;
|
||||
@@ -151,10 +152,10 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Create item error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/admin/items/[id] - 更新内容
|
||||
export async function PUT(request: NextRequest) {
|
||||
export const PUT = withCrypto(async (request: NextRequest) => {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
if (!id) return validationError('缺少 ID');
|
||||
@@ -218,10 +219,10 @@ export async function PUT(request: NextRequest) {
|
||||
console.error('Update item error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/items/[id] - 删除内容
|
||||
export async function DELETE(request: NextRequest) {
|
||||
export const DELETE = withCrypto(async (request: NextRequest) => {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
if (!id) return validationError('缺少 ID');
|
||||
@@ -250,4 +251,4 @@ export async function DELETE(request: NextRequest) {
|
||||
console.error('Delete item error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -28,6 +28,7 @@ function createMockRequest(options: {
|
||||
formData?: () => Promise<FormData>;
|
||||
}): NextRequest {
|
||||
return {
|
||||
headers: new Headers(),
|
||||
url: options.url || 'http://localhost/api/admin/media',
|
||||
formData: options.formData || (async () => new FormData()),
|
||||
} as unknown as NextRequest;
|
||||
|
||||
@@ -12,12 +12,13 @@ import {
|
||||
validationError,
|
||||
internalError,
|
||||
} from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
const MODEL_CODE = 'media';
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
// GET /api/admin/media - 获取媒体列表或按 ID 查询
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, MODEL_CODE, 'read');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -41,10 +42,10 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get media error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/media - 上传媒体文件(支持单文件/多文件)
|
||||
export async function POST(request: NextRequest) {
|
||||
export const POST = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, MODEL_CODE, 'create');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -94,10 +95,10 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Upload media error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/media?id=xxx - 删除媒体文件
|
||||
export async function DELETE(request: NextRequest) {
|
||||
export const DELETE = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, MODEL_CODE, 'delete');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -115,4 +116,4 @@ export async function DELETE(request: NextRequest) {
|
||||
}
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import { GET } from './route';
|
||||
|
||||
function createMockRequest(): NextRequest {
|
||||
return {
|
||||
headers: new Headers(),
|
||||
url: 'http://localhost/api/admin/models',
|
||||
} as unknown as NextRequest;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { requirePermission } from '@/lib/permissions';
|
||||
import { success, internalError } from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// GET /api/admin/models - 获取所有内容模型
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, 'content-model', 'read');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -21,4 +22,4 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get models error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -20,7 +20,7 @@ jest.unmock('./route');
|
||||
import { GET } from './route';
|
||||
|
||||
function createMockRequest(url: string): NextRequest {
|
||||
return { url } as unknown as NextRequest;
|
||||
return { headers: new Headers(), url } as unknown as NextRequest;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -2,9 +2,10 @@ import { NextRequest } from 'next/server';
|
||||
import { authenticateRequest } from '@/lib/auth';
|
||||
import { getUserNotifications } from '@/lib/cms/notifications';
|
||||
import { success, unauthorized, validationError, internalError } from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// GET /api/admin/notifications - 获取当前用户通知列表
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -27,4 +28,4 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get notifications error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ jest.unmock('./route');
|
||||
import { GET } from './route';
|
||||
|
||||
function createMockRequest(): NextRequest {
|
||||
return {} as unknown as NextRequest;
|
||||
return { headers: new Headers() } as unknown as NextRequest;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -2,9 +2,10 @@ import { NextRequest } from 'next/server';
|
||||
import { authenticateRequest } from '@/lib/auth';
|
||||
import { getUnreadCount } from '@/lib/cms/notifications';
|
||||
import { success, unauthorized, internalError } from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// GET /api/admin/notifications/unread-count - 获取当前用户未读通知数量
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -15,4 +16,4 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get unread count error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ import { GET, PUT } from './route';
|
||||
|
||||
function createMockRequest(body?: Record<string, unknown>): NextRequest {
|
||||
return {
|
||||
headers: new Headers(),
|
||||
json: async () => body ?? {},
|
||||
} as unknown as NextRequest;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { authenticateRequest } from '@/lib/auth';
|
||||
import { success, unauthorized, forbidden, internalError, validationError } from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// 内置角色,不允许删除
|
||||
const BUILTIN_ROLES = new Set(['super_admin', 'content_admin', 'content_editor', 'reviewer', 'readonly']);
|
||||
|
||||
// GET /api/admin/roles - 获取角色列表及其权限
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -46,10 +47,10 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get roles error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/admin/roles/:roleCode - 更新角色权限
|
||||
export async function PUT(request: NextRequest) {
|
||||
export const PUT = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -100,4 +101,4 @@ export async function PUT(request: NextRequest) {
|
||||
console.error('Update roles error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@ import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { authenticateRequest } from '@/lib/auth';
|
||||
import { success, unauthorized, internalError } from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// GET /api/admin/stats - 获取仪表盘统计数据
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -79,4 +80,4 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get stats error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2,9 +2,10 @@ import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { authenticateRequest, hashPassword } from '@/lib/auth';
|
||||
import { success, unauthorized, forbidden, internalError, validationError } from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// GET /api/admin/users - 获取用户列表
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -77,10 +78,10 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get users error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/users - 创建用户
|
||||
export async function POST(request: NextRequest) {
|
||||
export const POST = withCrypto(async (request: NextRequest) => {
|
||||
const user = authenticateRequest(request);
|
||||
if (!user) return unauthorized();
|
||||
|
||||
@@ -153,10 +154,10 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Create user error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/admin/users - 更新用户
|
||||
export async function PUT(request: NextRequest) {
|
||||
export const PUT = withCrypto(async (request: NextRequest) => {
|
||||
const currentUser = authenticateRequest(request);
|
||||
if (!currentUser) return unauthorized();
|
||||
|
||||
@@ -220,10 +221,10 @@ export async function PUT(request: NextRequest) {
|
||||
console.error('Update user error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/users - 删除用户
|
||||
export async function DELETE(request: NextRequest) {
|
||||
export const DELETE = withCrypto(async (request: NextRequest) => {
|
||||
const currentUser = authenticateRequest(request);
|
||||
if (!currentUser) return unauthorized();
|
||||
|
||||
@@ -260,4 +261,4 @@ export async function DELETE(request: NextRequest) {
|
||||
console.error('Delete user error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -42,6 +42,7 @@ function createMockRequest(options: {
|
||||
json?: () => Promise<Record<string, unknown>>;
|
||||
}): NextRequest {
|
||||
return {
|
||||
headers: new Headers(),
|
||||
url: options.url || 'http://localhost/api/admin/zones',
|
||||
json: options.json || (async () => ({})),
|
||||
} as unknown as NextRequest;
|
||||
|
||||
@@ -7,9 +7,10 @@ import {
|
||||
validationError,
|
||||
internalError,
|
||||
} from '@/lib/api-response';
|
||||
import { withCrypto } from '@/lib/api-crypto';
|
||||
|
||||
// GET /api/admin/zones - 获取所有内容区域
|
||||
export async function GET(request: NextRequest) {
|
||||
export const GET = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, 'content-zone', 'read');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -30,10 +31,10 @@ export async function GET(request: NextRequest) {
|
||||
console.error('Get zones error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/zones - 创建/更新内容区域
|
||||
export async function POST(request: NextRequest) {
|
||||
export const POST = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, 'content-zone', 'update');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -95,15 +96,15 @@ export async function POST(request: NextRequest) {
|
||||
console.error('Save zone error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/admin/zones - 更新区域设置
|
||||
export async function PUT(request: NextRequest) {
|
||||
export const PUT = withCrypto(async (request: NextRequest) => {
|
||||
return POST(request);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/zones/[id] - 删除内容区域
|
||||
export async function DELETE(request: NextRequest) {
|
||||
export const DELETE = withCrypto(async (request: NextRequest) => {
|
||||
const permission = await requirePermission(request, 'content-zone', 'delete');
|
||||
if ('response' in permission) return permission.response;
|
||||
|
||||
@@ -132,4 +133,4 @@ export async function DELETE(request: NextRequest) {
|
||||
console.error('Delete zone error:', error);
|
||||
return internalError();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user