247 lines
7.7 KiB
TypeScript
247 lines
7.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { getPublicProjectDb } from '@/lib/project-auth';
|
||
import { hashFingerprint, signLicense } from '@/lib/crypto';
|
||
import { verifyLicenseRequestSignature } from '@/lib/request-signature';
|
||
import { LICENSE_CONFIGS } from '@/lib/types';
|
||
import { createPendingAlertOnce, findFreeFingerprintReuse } from '@/lib/abuse-detection';
|
||
import { buildLicenseSigningPayload } from '@/lib/license-payload';
|
||
import { ensureDefaultModuleGrants } from '@/lib/module-grants';
|
||
import { z } from 'zod';
|
||
|
||
const registerFreeSchema = z.object({
|
||
fingerprint: z.object({
|
||
machineId: z.string({ required_error: '系统信息不完整:machineId 缺失' }),
|
||
hostname: z.string({ required_error: '系统信息不完整:hostname 缺失' }),
|
||
macAddress: z.string({ required_error: '系统信息不完整:macAddress 缺失' }),
|
||
cpuId: z.string().optional(),
|
||
instanceId: z.string({ required_error: '系统信息不完整:instanceId 缺失' }),
|
||
}),
|
||
customerName: z.string().min(1, '客户名称不能为空'),
|
||
contactInfo: z.string().min(1, '联系方式不能为空'),
|
||
timestamp: z.number({ required_error: '请求时间戳缺失' }),
|
||
});
|
||
|
||
function generateFreeActivationCode(): string {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||
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-FREE-${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 body = await request.json().catch(() => ({}));
|
||
const signatureResult = verifyLicenseRequestSignature('register-free', projectCode, body);
|
||
if (!signatureResult.valid) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '请求签名无效' },
|
||
{ status: signatureResult.response.status }
|
||
);
|
||
}
|
||
|
||
const parsed = registerFreeSchema.safeParse(body);
|
||
|
||
if (!parsed.success) {
|
||
const firstError = parsed.error.errors[0]?.message || '参数验证失败';
|
||
return NextResponse.json(
|
||
{ success: false, error: firstError },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
const { fingerprint, customerName, contactInfo, timestamp } = parsed.data;
|
||
const now = Date.now();
|
||
|
||
if (Math.abs(now - timestamp) > 5 * 60 * 1000) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '请求已过期' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
const existingActivation = await projectDbResult.prisma.activation.findFirst({
|
||
where: {
|
||
instanceId: fingerprint.instanceId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
license: true,
|
||
},
|
||
});
|
||
|
||
if (existingActivation) {
|
||
if (existingActivation.license.status === 'disabled') {
|
||
return NextResponse.json(
|
||
{ success: false, error: '许可证已被禁用' },
|
||
{ status: 403 }
|
||
);
|
||
}
|
||
|
||
if (
|
||
existingActivation.license.status === 'expired' ||
|
||
(existingActivation.license.expiresAt && existingActivation.license.expiresAt < new Date())
|
||
) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '许可证已过期' },
|
||
{ status: 403 }
|
||
);
|
||
}
|
||
|
||
await ensureDefaultModuleGrants(projectDbResult.prisma, {
|
||
id: existingActivation.license.id,
|
||
licenseType: existingActivation.license.licenseType,
|
||
expiresAt: existingActivation.license.expiresAt,
|
||
});
|
||
|
||
await projectDbResult.prisma.activation.update({
|
||
where: { id: existingActivation.id },
|
||
data: {
|
||
fingerprint: fingerprint as object,
|
||
lastHeartbeat: new Date(),
|
||
},
|
||
});
|
||
|
||
const signedLicense = signLicense(buildLicenseSigningPayload({
|
||
projectCode,
|
||
licenseKey: existingActivation.license.licenseKey,
|
||
licenseType: existingActivation.license.licenseType,
|
||
expiresAt: existingActivation.license.expiresAt,
|
||
fingerprint,
|
||
activationId: existingActivation.id,
|
||
}));
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
license: signedLicense,
|
||
data: {
|
||
activationCode: existingActivation.license.licenseKey,
|
||
plan: existingActivation.license.licenseType,
|
||
planName:
|
||
existingActivation.license.licenseType === 'FREE'
|
||
? '免费版'
|
||
: existingActivation.license.licenseType === 'STANDARD'
|
||
? '标准版'
|
||
: '专业版',
|
||
isExisting: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
const reuseResult = await findFreeFingerprintReuse(projectDbResult.prisma, fingerprint);
|
||
if (reuseResult.reused) {
|
||
const relatedLicense = await projectDbResult.prisma.license.findUnique({
|
||
where: { licenseKey: reuseResult.existingLicenseKey },
|
||
select: { id: true },
|
||
});
|
||
|
||
if (relatedLicense) {
|
||
await createPendingAlertOnce(projectDbResult.prisma, relatedLicense.id, 'free_reactivation_blocked', {
|
||
requestedInstanceId: fingerprint.instanceId,
|
||
existingInstanceId: reuseResult.existingInstanceId,
|
||
existingLicenseKey: reuseResult.existingLicenseKey,
|
||
matchScore: reuseResult.matchScore,
|
||
});
|
||
}
|
||
|
||
return NextResponse.json(
|
||
{
|
||
success: false,
|
||
error: '该设备已注册过免费版,请继续使用原免费许可证或升级付费版',
|
||
},
|
||
{ status: 409 }
|
||
);
|
||
}
|
||
|
||
const activationCode = generateFreeActivationCode();
|
||
let createdActivationId = '';
|
||
const clientIP =
|
||
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
||
request.headers.get('x-real-ip') ||
|
||
'unknown';
|
||
|
||
await projectDbResult.prisma.$transaction(async (tx) => {
|
||
const license = await tx.license.create({
|
||
data: {
|
||
licenseKey: activationCode,
|
||
licenseType: 'FREE',
|
||
customerName,
|
||
contactInfo,
|
||
maxUsers: LICENSE_CONFIGS.FREE.limits.maxUsers,
|
||
maxMaterials: LICENSE_CONFIGS.FREE.limits.maxMaterials,
|
||
features: LICENSE_CONFIGS.FREE.features,
|
||
expiresAt: null,
|
||
},
|
||
});
|
||
|
||
await ensureDefaultModuleGrants(tx, {
|
||
id: license.id,
|
||
licenseType: license.licenseType,
|
||
expiresAt: license.expiresAt,
|
||
});
|
||
|
||
const activation = await tx.activation.create({
|
||
data: {
|
||
licenseId: license.id,
|
||
instanceId: fingerprint.instanceId,
|
||
fingerprint: fingerprint as object,
|
||
lastHeartbeat: new Date(),
|
||
},
|
||
});
|
||
createdActivationId = activation.id;
|
||
|
||
await tx.heartbeatLog.create({
|
||
data: {
|
||
licenseId: license.id,
|
||
instanceId: fingerprint.instanceId,
|
||
fingerprintHash: hashFingerprint(fingerprint),
|
||
ipAddress: clientIP,
|
||
},
|
||
});
|
||
});
|
||
|
||
const signedLicense = signLicense(buildLicenseSigningPayload({
|
||
projectCode,
|
||
licenseKey: activationCode,
|
||
licenseType: 'FREE',
|
||
expiresAt: null,
|
||
fingerprint,
|
||
activationId: createdActivationId,
|
||
}));
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
license: signedLicense,
|
||
data: {
|
||
activationCode,
|
||
plan: 'FREE',
|
||
planName: '免费版',
|
||
isExisting: false,
|
||
},
|
||
});
|
||
} catch (error) {
|
||
console.error('项目免费版注册失败:', error);
|
||
return NextResponse.json(
|
||
{ success: false, error: '服务器内部错误' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|