This commit is contained in:
hsueh chiahao
2025-10-26 10:29:56 +08:00
parent b72142fc44
commit f92417d5e3
13 changed files with 225 additions and 153 deletions

View File

@@ -12,6 +12,8 @@ CREATE TABLE IF NOT EXISTS plugins (
plugin_id VARCHAR(255) UNIQUE NOT NULL,
plugin_name VARCHAR(255) NOT NULL,
private_key TEXT NOT NULL,
trial_date INTEGER DEFAULT 30,
rsa_version INTEGER DEFAULT 1,
deleted BOOLEAN DEFAULT FALSE
);

8
package-lock.json generated
View File

@@ -14,6 +14,7 @@
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jsrsasign": "^10.5.15",
"@types/node": "^20.8.9",
"@types/pg": "^8.10.7",
"tsx": "^4.6.2",
@@ -516,6 +517,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsrsasign": {
"version": "10.5.15",
"resolved": "https://registry.npmmirror.com/@types/jsrsasign/-/jsrsasign-10.5.15.tgz",
"integrity": "sha512-3stUTaSRtN09PPzVWR6aySD9gNnuymz+WviNHoTb85dKu+BjaV4uBbWWGykBBJkfwPtcNZVfTn2lbX00U+yhpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",

View File

@@ -5,8 +5,8 @@
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx src/server.ts",
"start": "node dist/server.js"
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.18.2",
@@ -15,6 +15,7 @@
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jsrsasign": "^10.5.15",
"@types/node": "^20.8.9",
"@types/pg": "^8.10.7",
"tsx": "^4.6.2",

13
src/app.ts Normal file
View File

@@ -0,0 +1,13 @@
import express from 'express';
import { setupLicenseRoutes } from './routes/licenseRoutes.js';
const app = express();
// 中间件
app.use(express.json());
// 设置路由
setupLicenseRoutes(app);
// 导出app而不是监听
export default app;

18
src/config/database.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Pool } from 'pg';
export const pool = new Pool({
user: 'postgres',
host: 'localhost',
database: 'kintone_license_server',
password: 'psadmin',
port: 5432,
});
pool.on('connect', () => {
console.log('Connected to PostgreSQL database');
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
process.exit(-1);
});

34
src/database/queries.ts Normal file
View File

@@ -0,0 +1,34 @@
import { pool } from '../config/database.js';
import { License, Plugin } from '../types/license.js';
export async function findLicense(domain: string, pluginId: string): Promise<License | null> {
const query = 'SELECT * FROM licenses WHERE domain = $1 AND plugin_id = $2';
const result = await pool.query(query, [domain, pluginId]);
return result.rows[0] || null;
}
export async function insertLicense(domain: string, pluginId: string, expiredTime: Date, pluginFk: number): Promise<License> {
const query = 'INSERT INTO licenses (domain, plugin_id, expired_time, plugin_fk) VALUES ($1, $2, $3, $4) RETURNING *';
const result = await pool.query(query, [domain, pluginId, expiredTime.toISOString(), pluginFk]);
return result.rows[0];
}
export async function findPluginByKey(pluginKey: string): Promise<Plugin | null> {
const query = 'SELECT id, plugin_id, plugin_name, private_key, trial_date, rsa_version, deleted FROM plugins WHERE plugin_id = $1 AND deleted = FALSE';
const result = await pool.query(query, [pluginKey]);
return result.rows[0] || null;
}
export async function findPluginById(id: number): Promise<Plugin | null> {
const query = 'SELECT id, plugin_id, plugin_name, private_key, trial_date, rsa_version, deleted FROM plugins WHERE id = $1 AND deleted = FALSE';
const result = await pool.query(query, [id]);
return result.rows[0] || null;
}
export async function getLicenseWithPlugin(license: License): Promise<{ license: License; plugin: Plugin }> {
const plugin = await findPluginById(license.plugin_fk);
if (!plugin) {
throw new Error('Plugin not found');
}
return { license, plugin };
}

26
src/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import app from './app.js';
import { pool } from './config/database.js';
const PORT = 3000;
const server = app.listen(PORT, () => {
console.log(`License server running on http://localhost:${PORT}`);
});
// 优雅关闭
function gracefulShutdown(signal: string): void {
console.log(`Received ${signal}. Shutting down server...`);
server.close(async () => {
try {
await pool.end();
console.log('Database connection closed.');
process.exit(0);
} catch (error) {
console.error('Error closing database connection:', error);
process.exit(1);
}
});
}
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));

View File

@@ -0,0 +1,27 @@
import { Router, Request, Response } from 'express';
import { processLicenseCheck } from '../services/licenseService.js';
const router = Router();
export function setupLicenseRoutes(router: Router): void {
router.post('/api/license/check', handleLicenseCheck);
router.get('/health', handleHealthCheck);
}
async function handleLicenseCheck(req: Request, res: Response): Promise<void> {
try {
const { domain, pluginId, pluginKey } = req.body;
const jwt = await processLicenseCheck(domain, pluginId, pluginKey);
res.json({
success: true,
jwt: jwt,
});
} catch (error) {
console.error('Error processing license check:', error);
res.status(400).json({ error: (error as Error).message });
}
}
function handleHealthCheck(req: Request, res: Response): void {
res.json({ status: 'OK' });
}

View File

@@ -1,149 +0,0 @@
import express from 'express';
import { Pool } from 'pg';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const KJUR = require('jsrsasign');
// 定义接口
interface LicenseInfo {
expiredTime: number;
isPaid: boolean;
domain: string;
pluginId: string;
fetchTime: number;
version: number;
}
// 数据库连接配置
const pool = new Pool({
user: 'postgres',
host: 'localhost',
database: 'kintone_license_server',
password: 'psadmin',
port: 5432,
});
const app = express();
const PORT = 3000;
// 中间件
app.use(express.json());
const TRIAL_DATE = 30
const RSA_VERSION = 1
// 许可证检查路由
app.post('/api/license/check', async (req, res) => {
try {
const { domain, pluginId, pluginKey } = req.body as { domain: string; pluginId: string; pluginKey: string };
if (!domain || !pluginId || !pluginKey) {
return res.status(400).json({ error: 'Missing domain, pluginId or pluginKey' });
}
// 查询是否已有许可证
const existingQuery = await pool.query(
'SELECT * FROM licenses WHERE domain = $1 AND plugin_id = $2',
[domain, pluginId]
);
let licenseData;
if (existingQuery.rows.length === 0) {
// 创建新的试用许可证30天有效期
const expiredDate = new Date();
expiredDate.setDate(expiredDate.getDate() + TRIAL_DATE);
expiredDate.setHours(23, 59, 59, 999);
// 查找 plugin_fk
const pluginQuery = await pool.query(
'SELECT id FROM plugins WHERE plugin_id = $1 AND deleted = FALSE',
[pluginKey]
);
if (pluginQuery.rows.length === 0) {
return res.status(400).json({ error: 'Invalid pluginKey' });
}
const pluginFk = pluginQuery.rows[0].id;
await pool.query(
'INSERT INTO licenses (domain, plugin_id, expired_time, plugin_fk) VALUES ($1, $2, $3, $4)',
[domain, pluginId, expiredDate.toISOString(), pluginFk]
);
// 重新查询新创建的记录
const newQuery = await pool.query(
'SELECT * FROM licenses WHERE domain = $1 AND plugin_id = $2',
[domain, pluginId]
);
licenseData = newQuery.rows[0];
} else {
licenseData = existingQuery.rows[0];
}
const isPaid = !!licenseData.purchase_date;
// 生成 JWT payload
const payload: LicenseInfo = {
expiredTime: isPaid ? -1 : new Date(licenseData.expired_time).getTime(), // 永久许可证则是 -1
isPaid,
domain: licenseData.domain,
pluginId: licenseData.plugin_id,
fetchTime: Date.now(),
version: RSA_VERSION,
};
// 获取私钥(对应的插件)
const privateKeyQuery = await pool.query(
'SELECT private_key FROM plugins WHERE id = $1',
[licenseData.plugin_fk]
);
const privateKey = privateKeyQuery.rows[0].private_key.trim();
// 创建 JWT header 和 payload
const header = {
alg: 'RS256',
typ: 'JWT'
};
const sHeader = JSON.stringify(header);
const jwtPayload = {
...payload,
iat: Math.floor(Date.now() / 1000)
};
const sPayload = JSON.stringify(jwtPayload);
// 生成 JWT
const jwt = KJUR.jws.JWS.sign(null, sHeader, sPayload, privateKey);
res.json({
success: true,
jwt: jwt,
});
} catch (error) {
console.error('Error processing license check:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'OK' });
});
// 启动服务器
app.listen(PORT, () => {
console.log(`License server running on http://localhost:${PORT}`);
});
// 优雅关闭
process.on('SIGINT', async () => {
console.log('Shutting down server...');
await pool.end();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('Shutting down server...');
await pool.end();
process.exit(0);
});

View File

@@ -0,0 +1,21 @@
import { KJUR } from 'jsrsasign';
import { LicenseInfo } from '../types/license.js';
export function generateJWT(key: string, licenseInfo: LicenseInfo): string {
const header = {
alg: 'RS256',
typ: 'JWT'
};
const jwtPayload = {
...licenseInfo,
iat: Math.floor(Date.now() / 1000)
};
const sHeader = JSON.stringify(header);
const sPayload = JSON.stringify(jwtPayload);
const privateKey = key.trim();
return KJUR.jws.JWS.sign(null, sHeader, sPayload, privateKey);
}

View File

@@ -0,0 +1,42 @@
import { LicenseInfo, License, Plugin } from '../types/license.js';
import { findLicense, insertLicense, findPluginByKey, getLicenseWithPlugin } from '../database/queries.js';
import { generateJWT } from './jwtService.js';
export function createLicenseInfo(license: License, plugin: Plugin): LicenseInfo {
const isPaid = !!license.purchase_date;
return {
expiredTime: isPaid ? -1 : new Date(license.expired_time).getTime(),
isPaid,
domain: license.domain,
pluginId: license.plugin_id,
fetchTime: Date.now(),
version: plugin.rsa_version,
};
}
export async function processLicenseCheck(domain: string, pluginId: string, pluginKey: string): Promise<string> {
if (!domain || !pluginId || !pluginKey) {
throw new Error('Missing domain, pluginId or pluginKey');
}
const plugin = await findPluginByKey(pluginKey);
if (!plugin) {
throw new Error('Invalid pluginKey');
}
let license = await findLicense(domain, pluginId);
if (!license) {
license = await createTrialLicense(domain, pluginId, plugin);
}
const licenseInfo = createLicenseInfo(license, plugin);
return generateJWT(plugin.private_key, licenseInfo);
}
async function createTrialLicense(domain: string, pluginId: string, plugin: Plugin): Promise<License> {
const expiredDate = new Date();
expiredDate.setDate(expiredDate.getDate() + plugin.trial_date);
expiredDate.setHours(23, 59, 59, 999);
return insertLicense(domain, pluginId, expiredDate, plugin.id);
}

29
src/types/license.ts Normal file
View File

@@ -0,0 +1,29 @@
export interface LicenseInfo {
expiredTime: number;
isPaid: boolean;
domain: string;
pluginId: string;
fetchTime: number;
version: number;
}
export interface Plugin {
id: number;
plugin_id: string;
plugin_name: string;
private_key: string;
trial_date: number;
rsa_version: number;
deleted: boolean;
}
export interface License {
id: number;
domain: string;
plugin_id: string;
expired_time: Date;
purchase_date: Date | null;
plugin_fk: number;
customer_fk: number | null;
message: string | null;
}

View File

@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"module": "node16",
"moduleResolution": "node16",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,