fix(cms): resolve admin dogfood findings on auth, content editing, and UX
修复 CMS / Admin 后台 dogfood 专项测试发现的阻塞性与体验性问题: - 登录后无限重定向(Secure Cookie + Edge Runtime JWT 兼容) - 内容新增/编辑保存 400/500(默认不加密请求体、自动生成唯一 slug) - 编辑时提交 status 导致验证错误 - 原生 confirm 阻塞自动化测试,改为 AlertDialog - 表单字段可访问性(id/htmlFor/fieldset) - JSON 数组标签字段新增 TagInput 组件 - 操作反馈统一使用 Sonner Toast - 后台隐藏营销 Cookie 横幅 - 媒体库空状态优化 添加 dogfood-cms-regression 与 dogfood-cms-output 报告、截图及工作流路由测试。
This commit is contained in:
+17
-1
@@ -13,7 +13,8 @@ class AdminApiClient {
|
||||
options: RequestInit & { encrypt?: boolean } = {},
|
||||
): Promise<T> {
|
||||
const token = this.getToken();
|
||||
const shouldEncrypt = options.encrypt !== false && token;
|
||||
// 当前服务端 API 未实现请求体解密,默认不加密;如需加密,调用方显式传入 encrypt: true。
|
||||
const shouldEncrypt = options.encrypt === true && token;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
...(options.headers as Record<string, string>),
|
||||
@@ -173,6 +174,21 @@ class AdminApiClient {
|
||||
zones: Array.isArray(zones) ? zones.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
async getRoles() {
|
||||
return this.request<{
|
||||
roles: Array<{ code: string; name: string; builtin: boolean }>;
|
||||
permissions: Array<{ roleCode: string; modelCode: string; action: string }>;
|
||||
models: Array<{ code: string; name: string }>;
|
||||
}>('/api/admin/roles');
|
||||
}
|
||||
|
||||
async updateRolePermissions(roleCode: string, permissions: Array<{ modelCode: string; action: string }>) {
|
||||
return this.request<{ roleCode: string; permissions: string[] }>('/api/admin/roles', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ roleCode, permissions }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const adminApi = new AdminApiClient();
|
||||
+35
-24
@@ -2,11 +2,19 @@ import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { type NextRequest } from 'next/server';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'novalon-cms-secret-key-2024';
|
||||
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'novalon-cms-refresh-secret-2024';
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
|
||||
const TOKEN_EXPIRY = '24h';
|
||||
const REFRESH_TOKEN_EXPIRY = '7d';
|
||||
|
||||
if (!JWT_SECRET || !JWT_REFRESH_SECRET) {
|
||||
throw new Error(
|
||||
'JWT_SECRET and JWT_REFRESH_SECRET must be set in environment variables. ' +
|
||||
'Do not run the application with default or empty secrets.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export interface JwtPayload {
|
||||
userId: string;
|
||||
username: string;
|
||||
@@ -26,17 +34,17 @@ export async function verifyPassword(password: string, hash: string): Promise<bo
|
||||
// ============ Token 工具 ============
|
||||
|
||||
export function generateTokens(payload: JwtPayload): { accessToken: string; refreshToken: string } {
|
||||
const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY });
|
||||
const refreshToken = jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
|
||||
const accessToken = jwt.sign(payload, JWT_SECRET!, { expiresIn: TOKEN_EXPIRY });
|
||||
const refreshToken = jwt.sign(payload, JWT_REFRESH_SECRET!, { expiresIn: REFRESH_TOKEN_EXPIRY });
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
export function verifyAccessToken(token: string): JwtPayload {
|
||||
return jwt.verify(token, JWT_SECRET) as JwtPayload;
|
||||
return jwt.verify(token, JWT_SECRET!) as JwtPayload;
|
||||
}
|
||||
|
||||
export function verifyRefreshToken(token: string): JwtPayload {
|
||||
return jwt.verify(token, JWT_REFRESH_SECRET) as JwtPayload;
|
||||
return jwt.verify(token, JWT_REFRESH_SECRET!) as JwtPayload;
|
||||
}
|
||||
|
||||
// ============ 请求认证 ============
|
||||
@@ -61,24 +69,27 @@ export function authenticateRequest(request: NextRequest): JwtPayload | null {
|
||||
|
||||
// ============ Cookie 工具 ============
|
||||
|
||||
export function setTokenCookie(response: Response, accessToken: string, refreshToken: string): void {
|
||||
response.headers.set(
|
||||
'Set-Cookie',
|
||||
`novalon_token=${accessToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
|
||||
);
|
||||
response.headers.append(
|
||||
'Set-Cookie',
|
||||
`novalon_refresh=${refreshToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`
|
||||
);
|
||||
function buildCookie(name: string, value: string, maxAge: number, secure = false): string {
|
||||
const secureFlag = secure ? '; Secure' : '';
|
||||
return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax${secureFlag}; Max-Age=${maxAge}`;
|
||||
}
|
||||
|
||||
export function clearTokenCookie(response: Response): void {
|
||||
response.headers.set(
|
||||
'Set-Cookie',
|
||||
`novalon_token=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`
|
||||
);
|
||||
response.headers.append(
|
||||
'Set-Cookie',
|
||||
`novalon_refresh=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`
|
||||
);
|
||||
/**
|
||||
* 设置认证 cookie。
|
||||
* @param secure 是否添加 Secure 标志。应根据当前请求协议决定(https 为 true),
|
||||
* 避免在本地 http 预览/开发环境设置 Secure 导致浏览器拒绝保存 cookie。
|
||||
*/
|
||||
export function setTokenCookie(
|
||||
response: Response,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
secure = false,
|
||||
): void {
|
||||
response.headers.set('Set-Cookie', buildCookie('novalon_token', accessToken, 86400, secure));
|
||||
response.headers.append('Set-Cookie', buildCookie('novalon_refresh', refreshToken, 604800, secure));
|
||||
}
|
||||
|
||||
export function clearTokenCookie(response: Response, secure = false): void {
|
||||
response.headers.set('Set-Cookie', buildCookie('novalon_token', '', 0, secure));
|
||||
response.headers.append('Set-Cookie', buildCookie('novalon_refresh', '', 0, secure));
|
||||
}
|
||||
Reference in New Issue
Block a user