refactor
This commit is contained in:
@@ -12,6 +12,8 @@ CREATE TABLE IF NOT EXISTS plugins (
|
|||||||
plugin_id VARCHAR(255) UNIQUE NOT NULL,
|
plugin_id VARCHAR(255) UNIQUE NOT NULL,
|
||||||
plugin_name VARCHAR(255) NOT NULL,
|
plugin_name VARCHAR(255) NOT NULL,
|
||||||
private_key TEXT NOT NULL,
|
private_key TEXT NOT NULL,
|
||||||
|
trial_date INTEGER DEFAULT 30,
|
||||||
|
rsa_version INTEGER DEFAULT 1,
|
||||||
deleted BOOLEAN DEFAULT FALSE
|
deleted BOOLEAN DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsrsasign": "^10.5.15",
|
||||||
"@types/node": "^20.8.9",
|
"@types/node": "^20.8.9",
|
||||||
"@types/pg": "^8.10.7",
|
"@types/pg": "^8.10.7",
|
||||||
"tsx": "^4.6.2",
|
"tsx": "^4.6.2",
|
||||||
@@ -516,6 +517,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsx src/server.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"start": "node dist/server.js"
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsrsasign": "^10.5.15",
|
||||||
"@types/node": "^20.8.9",
|
"@types/node": "^20.8.9",
|
||||||
"@types/pg": "^8.10.7",
|
"@types/pg": "^8.10.7",
|
||||||
"tsx": "^4.6.2",
|
"tsx": "^4.6.2",
|
||||||
|
|||||||
13
src/app.ts
Normal file
13
src/app.ts
Normal 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
18
src/config/database.ts
Normal 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
34
src/database/queries.ts
Normal 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
26
src/index.ts
Normal 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'));
|
||||||
27
src/routes/licenseRoutes.ts
Normal file
27
src/routes/licenseRoutes.ts
Normal 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' });
|
||||||
|
}
|
||||||
149
src/server.ts
149
src/server.ts
@@ -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);
|
|
||||||
});
|
|
||||||
21
src/services/jwtService.ts
Normal file
21
src/services/jwtService.ts
Normal 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);
|
||||||
|
}
|
||||||
42
src/services/licenseService.ts
Normal file
42
src/services/licenseService.ts
Normal 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
29
src/types/license.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "Node16",
|
"module": "node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "node16",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user