175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
import { createHash } from 'crypto';
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getPublicProjectDb } from '@/lib/project-auth';
|
|
import { verifyApiKey } from '@/lib/api-key';
|
|
import { LICENSE_CONFIGS } from '@/lib/types';
|
|
import { ensureDefaultModuleGrants } from '@/lib/module-grants';
|
|
import { z } from 'zod';
|
|
|
|
const generateSchema = z.object({
|
|
orderNo: z.string().min(1, '订单号不能为空'),
|
|
plan: z.enum(['STANDARD', 'PRO'], {
|
|
errorMap: () => ({ message: '许可证类型必须是 STANDARD 或 PRO' }),
|
|
}),
|
|
customerEmail: z.string().email('客户邮箱格式无效'),
|
|
customerName: z.string().optional(),
|
|
contactInfo: z.string().optional(),
|
|
validDays: z.number().int().min(1).max(3650).optional(),
|
|
timestamp: z.number().int(),
|
|
sign: z.string().min(1, '签名不能为空'),
|
|
});
|
|
|
|
const SIGN_EXPIRE_MS = 5 * 60 * 1000;
|
|
|
|
const PLAN_NAMES: Record<string, string> = {
|
|
STANDARD: '标准版',
|
|
PRO: '专业版',
|
|
};
|
|
|
|
function generateActivationCode(plan: 'STANDARD' | 'PRO'): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
const typePrefix = plan === 'PRO' ? 'PRO' : 'STD';
|
|
const segments: string[] = [];
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
let segment = '';
|
|
for (let j = 0; j < 4; j++) {
|
|
segment += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
segments.push(segment);
|
|
}
|
|
|
|
return `FLNXI-${typePrefix}-${segments.join('-')}`;
|
|
}
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ projectCode: string }> }
|
|
) {
|
|
try {
|
|
const { projectCode } = await params;
|
|
const projectDbResult = await getPublicProjectDb(projectCode);
|
|
|
|
if (!projectDbResult.success) {
|
|
return projectDbResult.response;
|
|
}
|
|
|
|
const apiKey = request.headers.get('X-API-Key');
|
|
const authResult = await verifyApiKey(apiKey, projectDbResult.prisma);
|
|
|
|
if (!authResult.valid) {
|
|
return NextResponse.json(
|
|
{ success: false, error: authResult.error || 'API Key 无效或已过期' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const body = await request.json().catch(() => null);
|
|
const parsed = generateSchema.safeParse(body);
|
|
|
|
if (!parsed.success) {
|
|
const firstError = parsed.error.errors[0]?.message || '参数验证失败';
|
|
return NextResponse.json(
|
|
{ success: false, error: firstError },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const {
|
|
orderNo,
|
|
plan,
|
|
customerEmail,
|
|
customerName,
|
|
contactInfo,
|
|
validDays,
|
|
timestamp,
|
|
sign,
|
|
} = parsed.data;
|
|
|
|
if (Math.abs(Date.now() - timestamp) > SIGN_EXPIRE_MS) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '请求已过期,请重新发起' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const expectedSign = createHash('md5')
|
|
.update(`${orderNo}${timestamp}${apiKey}`)
|
|
.digest('hex');
|
|
|
|
if (sign.toLowerCase() !== expectedSign.toLowerCase()) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '签名验证失败' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const existingOrder = await projectDbResult.prisma.order.findUnique({
|
|
where: { orderNo },
|
|
});
|
|
|
|
if (existingOrder) {
|
|
return NextResponse.json(
|
|
{ success: false, error: '订单号已存在' },
|
|
{ status: 409 }
|
|
);
|
|
}
|
|
|
|
const activationCode = generateActivationCode(plan);
|
|
const config = LICENSE_CONFIGS[plan];
|
|
const expiresAt = validDays
|
|
? new Date(Date.now() + validDays * 24 * 60 * 60 * 1000)
|
|
: null;
|
|
|
|
await projectDbResult.prisma.$transaction(async (tx) => {
|
|
const license = await tx.license.create({
|
|
data: {
|
|
licenseKey: activationCode,
|
|
licenseType: plan,
|
|
customerName: customerName || null,
|
|
contactInfo: contactInfo || null,
|
|
maxUsers: config.limits.maxUsers,
|
|
maxMaterials: config.limits.maxMaterials,
|
|
features: config.features,
|
|
expiresAt,
|
|
},
|
|
});
|
|
|
|
await ensureDefaultModuleGrants(tx, {
|
|
id: license.id,
|
|
licenseType: license.licenseType,
|
|
expiresAt: license.expiresAt,
|
|
});
|
|
|
|
await tx.order.create({
|
|
data: {
|
|
orderNo,
|
|
plan,
|
|
customerEmail,
|
|
customerName: customerName || null,
|
|
licenseId: license.id,
|
|
validDays: validDays ?? undefined,
|
|
},
|
|
});
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: {
|
|
activationCode,
|
|
plan,
|
|
planName: PLAN_NAMES[plan],
|
|
expiresAt: expiresAt ? expiresAt.toISOString() : null,
|
|
validDays: validDays || null,
|
|
isPermanent: !validDays,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('[License] 项目激活码生成失败:', error);
|
|
return NextResponse.json(
|
|
{ success: false, error: '服务器内部错误' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|