This commit is contained in:
2025-10-21 08:10:15 +00:00
commit 8eaf13f445
1983 changed files with 292430 additions and 0 deletions

20
farmeworkapi/.env Normal file
View File

@@ -0,0 +1,20 @@
# Database configuration
DB_HOST=est_mysql
DB_PORT=3306
DB_USER=root
DB_PASSWORD=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDQMYcjqnrMnr9G
DB_NAME=login
# SurveyKing_DB_NAME
SurveyKing_DB_HOST=est_mysql
SurveyKing_DB_PORT=3306
SurveyKing_DB_USER=root
SurveyKing_DB_PASSWORD=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDQMYcjqnrMnr9G
SurveyKing_DB_NAME=surveyking
# JWT configuration
JWT_SECRET=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDQMYcjqnrMnr9G
# Server configuration
PORT=3000

0
farmeworkapi/admin.log Normal file
View File

1215
farmeworkapi/farmeworkapi.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
{
"payload": "eyJtb2RlbCI6IkVTVC0xMDAiLCJ1c2VyIjoi5LiK5rW35pyX5Z2k5L+h5oGv57O757uf5pyJ6ZmQ5YWs5Y+4IiwiaGFyZHdhcmVfaWQiOiJBQjAwNFoyNzIwMDM0MCIsInNlcmlhbCI6IlNOLUtGWVVUSkE0LTI5MjUyMiIsImFjdGl2YXRpb25fY29kZSI6IkFDVC1aQ0I0VFQtUDlTNyIsImFjdGl2YXRlZF9hdCI6IjIwMjUtMTAtMTZUMDE6MTU6MDguOTM1WiIsImV4cGlyZXNfYXQiOiIyMDI2LTEwLTE2VDAxOjE1OjA4LjkzNVoiLCJnb2xkX3NlcnZpY2VfZXhwaXJlc19hdCI6IjIwMjYtMTAtMTZUMDE6MTU6MDguOTM1WiIsImlzc3VlZF9hdCI6IjIwMjUtMTAtMTZUMDE6MTU6MDguOTM1WiIsImlzc3VlciI6IuS4iua1t+acl+WdpOS/oeaBr+ezu+e7n+aciemZkOWFrOWPuCJ9",
"signature": "aLSKrijna6j8Oe00Ol0OhhBv9QAhBZyJCJC11RcwsvNeGgFlmAwo+OOW2SupT/w4DZMe3NXubbBDjAgZYyT27TvjEn/WiOga3PgnbcrB65MNNEH+UaXWf0R4sfI0k7R4j+LeK4MktHv6DPyv+bumzVGxEgHCN9xl6UWRJ8qcjEbUvG9mCGofDPOoXZzuPes+cDFF3yIR3lWiZl+8uHq79biyET3fZMUT72TEQf9NqJCEUudRKgagmkDSboeoWdB+0wHaVthoZpzhdW1kn5YByI+Fy/2F2xWFra/1LKJUkcsKdzVtGGXEs+2V1CIlONod+Cb4r4zbctqZFqbvvUK2EA=="
}

View File

@@ -0,0 +1,303 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>License 发行端(浏览器)</title>
<style>
body{font-family:system-ui,-apple-system,'Helvetica Neue',Arial;padding:20px;background:#0f172a;color:#e6eef8}
.card{background:#0b1220;padding:18px;border-radius:8px;box-shadow:0 6px 18px rgba(2,6,23,0.6);max-width:900px;margin:12px auto}
label{display:block;margin-top:10px;font-size:14px}
input, select, textarea{width:100%;padding:8px;margin-top:6px;border-radius:6px;border:1px solid #223;}
button{margin-top:12px;padding:10px 12px;border-radius:8px;border:none;cursor:pointer;background:#4f46e5;color:white}
.row{display:flex;gap:12px}
.col{flex:1}
pre{background:#071026;padding:12px;border-radius:6px;overflow:auto}
.small{font-size:13px;color:#9fb0d4}
</style>
</head>
<body>
<div class="card">
<h2>License 发行端(浏览器版)</h2>
<p class="small">在本页面生成公私钥对、序列号、激活码,并签名生成 .lic 文件。私钥仅保存在浏览器生成后允许你下载,<strong>请妥善保管私钥</strong></p>
<label>型号 (model)
<select id="inputModel" class="form-select">
<option value="">请选择型号</option>
<option value="EST-05E">EST-05E</option>
<option value="EST-10E">EST-10E</option>
<option value="EST-100E">EST-100E</option>
<option value="EST-05C">EST-05C</option>
<option value="EST-10C">EST-10C</option>
<option value="EST-100C">EST-100C</option>
<option value="EST-10A">EST-10A</option>
<option value="EST-100">EST-100</option>
</select>
</label>
<label>硬件码 (hardware_id)
<input id="inputHardwareId" placeholder="请输入硬件码" />
</label>
<label>用户 (user)
<input id="inputUser" placeholder="请输入最终用户" />
</label>
<div class="row">
<div class="col">
<label>激活天数(默认无限期)
<input id="inputDays" type="number" value="99999" readonly />
</label>
</div>
<div class="col">
<label>金牌服务有效期
<select id="inputGoldDays">
<option value="90">3个月</option>
<option value="180">6个月</option>
<option value="365">1年</option>
<option value="730">2年</option>
<option value="1095">3年</option>
<option value="1825">5年</option>
</select>
</label>
</div>
</div>
<label>序列号 生成规则
<div class="small">自动生成SN-随机8字符-时间戳</div>
<button id="btnGenSerial" type="button">生成序列号</button>
<input id="inputSerial" placeholder="点击生成或手动输入" />
</label>
<label>激活码
<button id="btnGenAct" type="button">生成激活码</button>
<input id="inputAct" placeholder="点击生成或手动输入" />
</label>
<hr style="border:none;border-top:1px solid #173047;margin:14px 0" />
<div class="row">
<div class="col">
<button id="btnCreateLicense">生成并签名 .lic 文件</button>
</div>
</div>
<div class="row" style="margin-top:10px">
<div class="col">
<label>导入私钥 (PKCS8 PEM 格式)
<input type="file" id="inputPrivKeyFile" accept=".pem,.key" />
</label>
</div>
<div class="col">
<label>或粘贴私钥内容
<textarea id="inputPrivKeyText" rows="3" placeholder="-----BEGIN PRIVATE KEY-----&#10;...&#10;-----END PRIVATE KEY-----"></textarea>
</label>
<button id="btnImportPrivKey" style="margin-top:6px">导入私钥</button>
</div>
</div>
<p class="small">生成后你可以下载license 文件。私钥默认从 priv.pem 自动导入,也可以手动导入已存在的私钥用于签名。</p>
<h3>调试输出</h3>
<pre id="out">准备就绪</pre>
<h3>私钥(仅供发行方保存)</h3>
<pre id="privpem">(请导入私钥)</pre>
</div>
<script>
// helpers
function rndStr(len=8){const chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789';let s='';for(let i=0;i<len;i++)s+=chars[Math.floor(Math.random()*chars.length)];return s}
function ab2b64(buf){return btoa(String.fromCharCode(...new Uint8Array(buf)));}
function b642ab(b64){const bin=atob(b64);const len=bin.length;const arr=new Uint8Array(len);for(let i=0;i<len;i++)arr[i]=bin.charCodeAt(i);return arr.buffer;}
// PEM parsing
function parsePem(pem) {
// 移除头尾和换行符获取base64内容
const pemContent = pem.replace(/-----BEGIN [^-]+-----/, '')
.replace(/-----END [^-]+-----/, '')
.replace(/[\r\n]/g, '');
return b642ab(pemContent);
}
// 从PEM导入私钥
async function importPrivateKeyFromPem(pemString) {
try {
const binaryDer = parsePem(pemString);
return await window.crypto.subtle.importKey(
'pkcs8',
binaryDer,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'},
},
true,
['sign']
);
} catch (error) {
console.error('导入私钥失败:', error);
throw new Error('导入私钥失败: ' + error.message);
}
}
// generate RSA keypair
let issuerKeyPair = null;
// 导入私钥
document.getElementById('btnImportPrivKey').addEventListener('click', async () => {
try {
// 检查是否有文件上传
const fileInput = document.getElementById('inputPrivKeyFile');
const textInput = document.getElementById('inputPrivKeyText');
let pemContent = '';
if (fileInput.files.length > 0) {
// 从文件读取
const file = fileInput.files[0];
pemContent = await file.text();
} else if (textInput.value.trim()) {
// 从文本框读取
pemContent = textInput.value.trim();
} else {
throw new Error('请上传私钥文件或粘贴私钥内容');
}
setOut('正在导入私钥...');
// 导入私钥
const privateKey = await importPrivateKeyFromPem(pemContent);
// 设置私钥
if (!issuerKeyPair) {
issuerKeyPair = {};
}
issuerKeyPair.privateKey = privateKey;
// 更新UI
document.getElementById('privpem').textContent = pemContent;
setOut('私钥导入成功现在可以使用此私钥生成license文件。');
} catch (error) {
setOut('导入私钥失败: ' + error.message);
console.error(error);
}
});
// 从文件读取私钥
document.getElementById('inputPrivKeyFile').addEventListener('change', async (event) => {
try {
if (event.target.files.length > 0) {
const file = event.target.files[0];
const pemContent = await file.text();
document.getElementById('inputPrivKeyText').value = pemContent;
}
} catch (error) {
setOut('读取私钥文件失败: ' + error.message);
console.error(error);
}
});
function downloadText(text, filename){
const a=document.createElement('a');
const blob=new Blob([text],{type:'application/octet-stream'});
a.href=URL.createObjectURL(blob);
a.download=filename;
document.body.appendChild(a);
a.click();
a.remove();
}
// generate serial
document.getElementById('btnGenSerial').addEventListener('click', ()=>{
const sn = `SN-${rndStr(8)}-${Date.now().toString().slice(-6)}`;
document.getElementById('inputSerial').value = sn;
setOut('已生成序列号: '+sn);
});
// generate activation code
document.getElementById('btnGenAct').addEventListener('click', ()=>{
const act = `ACT-${rndStr(6)}-${rndStr(4)}`;
document.getElementById('inputAct').value = act;
setOut('已生成激活码: '+act);
});
function isoNow(){return new Date().toISOString();}
function isoAddDays(days){const d=new Date();d.setUTCDate(d.getUTCDate()+Number(days));return d.toISOString();}
// sign payload using private key
async function signPayload(payloadStr){
if(!issuerKeyPair) throw new Error('请先生成/导入私钥');
const enc = new TextEncoder().encode(payloadStr);
const signature = await window.crypto.subtle.sign({name:'RSASSA-PKCS1-v1_5'}, issuerKeyPair.privateKey, enc);
return ab2b64(signature);
}
// create license file and download
document.getElementById('btnCreateLicense').addEventListener('click', async ()=>{
try{
const model = document.getElementById('inputModel').value.trim();
const user = document.getElementById('inputUser').value.trim();
const hardware_id = document.getElementById('inputHardwareId').value.trim();
const serial = document.getElementById('inputSerial').value.trim() || `SN-${rndStr(8)}-${Date.now().toString().slice(-6)}`;
const activation_code = document.getElementById('inputAct').value.trim() || `ACT-${rndStr(6)}-${rndStr(4)}`;
const days = Number(document.getElementById('inputDays').value || 365);
const goldDays = Number(document.getElementById('inputGoldDays').value || 365);
if(!model||!user||!hardware_id){setOut('请填写型号、用户和硬件码');return}
setOut('生成 license 中,请稍候...');
const payloadObj = {
model,
user,
hardware_id,
serial,
activation_code,
activated_at: isoNow(),
expires_at: isoAddDays(days),
gold_service_expires_at: isoAddDays(goldDays),
issued_at: isoNow(),
issuer: '上海朗坤信息系统有限公司'
};
const payloadStr = JSON.stringify(payloadObj);
function base64EncodeUnicode(str) {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) =>
String.fromCharCode('0x' + p1)
)
);
}
const payloadBase64 = base64EncodeUnicode(JSON.stringify(payloadObj));
const signatureB64 = await signPayload(payloadStr);
const lic = {
payload: payloadBase64,
signature: signatureB64
};
const licText = JSON.stringify(lic, null, 2);
// download license file
downloadText(licText, `${serial}.lic`);
// update UI
setOut('license 已生成并下载:'+serial+'.lic\n\npayload:\n'+payloadStr);
}catch(err){
setOut('生成失败: '+err.message);
console.error(err);
}
});
function setOut(text){document.getElementById('out').textContent = text}
// allow importing an existing private key (pkcs8 PEM)
// (optional) not implemented UI; could add in future
</script>
</body>
</html>

15
farmeworkapi/network Normal file
View File

@@ -0,0 +1,15 @@
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=dhcp
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=enp3s0
UUID=558d7602-4adc-4ed5-be2f-396676602023
DEVICE=enp3s0
ONBOOT=yes

28
farmeworkapi/priv.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDQMYcjqnrMnr9G
8kxaG9mJ3ENGHFH2EjzdOYG4+hI4kcOdUM3j1mwIOsOjr6liGA5Nmq2FtjtoimSI
5mC040B+OBGLbji949DqeEbYkqxxLN534QAkALLumydbNmCq905mFzpNePJnRwDB
lVPxR4I09cV8rFn1znElMb7e3JBD+7hYoUVyunQdGLT3VrVE6guG6jJ4US1NgO21
RRGn7cc+IvUYlWVeORbMnJU5r/j20BU/dLsHlYS2sbaF5YY19U/gs9VmU7f+NPD3
wsofvjBUdk2u3ZEyaUcDL4Q9jcBywRBFGbM8I67MwpQWhX7fbSlvFXY6xQTB9PfC
xA9t4d0HAgMBAAECggEABpzEF4hnHQ+fsJ+qeCQxTRUfV983ZTLgFwCoE5lxsbVD
esAhwe5CASLoqs69+P0dmy5h+4C1s1TN66Exih07o6ahPAX0rGdLb98BFQjXjEKq
EDGnwPz6seRgpB9+g3+WUFzFf8drH4ecSQm2oE+unYL+0j5hHxL++fjyAH8N3/LU
kM6GKoWN2ZEPmcMCwPlIPNgYkQdsIAfwF73IWMShBWakBNyL/xFBv2jrXfIj1kbx
Bih0/SmrNPbxQWdHUYNhjL/vNZJfaxe62zJ1V8cFZqsMGXgjDmsSw1gjEo+WU9bs
Mzeit71Jn5D4RHcK1EJ+0K2cdi8oqIDJLnM6jZoHNQKBgQDzvgxrzzhN+zc2zQon
d4vWM4JDxg/vHkcMGLQVGPUL0qE3XbfYC9D1pxkCC8rdraWcxNyiyZ6DvCxY+YNW
tDiNVU3CeZYEwpu5YrSsNA3RzEr28Gs8G253jzWUzwRdvowe9NzxH+SIMMvEnisR
sTST+RHomMy0DkUpdPzMxarfnQKBgQDaqdIpwoks0gLNVHhltb/1O8BPeQFU4ayd
NUWFvuDl+F6zB5ocSBspXcKyASBaJJqpDAewPaNTjH1YGCfM5VVRr84gg6Myh6YD
oPJBT/Dqh95A+RU6Jgej7OLS38oHlCM6Om1YuYxIBL0WgPxdpZpJ7MSgqNeuZiZI
R3ScpNKX8wKBgFEDry86ThWlVUHSvvhFROZqcnbCY1NwNd8watLEW9aVKN61OMEH
lGnCI47JiqFJ8Uz9JfS5nQVeS0M7k8sC1fUsEE9+iD2Gzam9xB2ko59DDvurB4mP
+JSmtgGDGYlrJzm226UNm7EkvuXYSKp1wusYEzrDZu7F+3j/GHObk+MFAoGAXJPX
wgx2DX7g5tQjoVRg8FAa7MrbkHd3oJukUqrtBxCv73aBfgXdUhc7TigB7Yhf3sCU
JL8D/mSV0Q1xFGPoSox3Zfa1yrf5xsGGr7ZL1tAtfi63GQZu561sn0FWhPzoKUgB
cbYlOXygHf4AWxm5CCFxYl/56QGgpatzKLrz2pcCgYAkWuAP4QNLPluSzmkWigxY
XyZuEnNdu9WZtBwR5RZ8It4KSRiUWduQ7AousEgB81HOTSmHeCF33r59TCC6DBkF
83FULNov3YY7UiLJv5AVM+zxJwG+aiQtoV3Bj1u698hT6rn6pMItT6TCpL3ykDnj
++DJbia17ChCIriKpku5Xg==
-----END PRIVATE KEY-----

9
farmeworkapi/pub.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0DGHI6p6zJ6/RvJMWhvZ
idxDRhxR9hI83TmBuPoSOJHDnVDN49ZsCDrDo6+pYhgOTZqthbY7aIpkiOZgtONA
fjgRi244vePQ6nhG2JKscSzed+EAJACy7psnWzZgqvdOZhc6TXjyZ0cAwZVT8UeC
NPXFfKxZ9c5xJTG+3tyQQ/u4WKFFcrp0HRi091a1ROoLhuoyeFEtTYDttUURp+3H
PiL1GJVlXjkWzJyVOa/49tAVP3S7B5WEtrG2heWGNfVP4LPVZlO3/jTw98LKH74w
VHZNrt2RMmlHAy+EPY3AcsEQRRmzPCOuzMKUFoV+320pbxV2OsUEwfT3wsQPbeHd
BwIDAQAB
-----END PUBLIC KEY-----

12
farmeworkapi/server.log Normal file
View File

@@ -0,0 +1,12 @@
2025-10-21T07:24:48.007Z - 正在验证 License 文件...
2025-10-21T07:24:48.012Z - Error connecting to the database: Error: getaddrinfo EAI_AGAIN est_mysql
2025-10-21T07:24:48.012Z - Error connecting to the SurveyKing database: Error: getaddrinfo EAI_AGAIN est_mysql
2025-10-21T07:24:48.012Z - 验证最新的 License 文件: EST-100.lic
2025-10-21T07:24:48.014Z - 成功读取公钥文件 pub.pem
2025-10-21T07:24:48.018Z - 读取硬件序列号失败: ENOENT: no such file or directory, open '/hardware_serial'
2025-10-21T07:24:48.018Z - 验证 License 文件失败 (EST-100.lic): ENOENT: no such file or directory, open '/hardware_serial'
2025-10-21T07:24:48.019Z - License 验证失败: EST-100.lic
2025-10-21T07:24:48.019Z - 签名验证失败
2025-10-21T07:24:48.019Z - 硬件码不匹配
2025-10-21T07:24:48.019Z - 错误信息: ENOENT: no such file or directory, open '/hardware_serial'
2025-10-21T07:24:48.019Z - License 验证完成