From f92417d5e3fc6187d2211835f80c8fe76fa4cbb2 Mon Sep 17 00:00:00 2001 From: hsueh chiahao Date: Sun, 26 Oct 2025 10:29:56 +0800 Subject: [PATCH] refactor --- init-db.sql | 2 + package-lock.json | 8 ++ package.json | 5 +- src/app.ts | 13 +++ src/config/database.ts | 18 ++++ src/database/queries.ts | 34 ++++++++ src/index.ts | 26 ++++++ src/routes/licenseRoutes.ts | 27 ++++++ src/server.ts | 149 --------------------------------- src/services/jwtService.ts | 21 +++++ src/services/licenseService.ts | 42 ++++++++++ src/types/license.ts | 29 +++++++ tsconfig.json | 4 +- 13 files changed, 225 insertions(+), 153 deletions(-) create mode 100644 src/app.ts create mode 100644 src/config/database.ts create mode 100644 src/database/queries.ts create mode 100644 src/index.ts create mode 100644 src/routes/licenseRoutes.ts delete mode 100644 src/server.ts create mode 100644 src/services/jwtService.ts create mode 100644 src/services/licenseService.ts create mode 100644 src/types/license.ts diff --git a/init-db.sql b/init-db.sql index f78814a..cacf950 100644 --- a/init-db.sql +++ b/init-db.sql @@ -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 ); diff --git a/package-lock.json b/package-lock.json index 25de7fd..ee287bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index dffd09d..34558d5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..e7df41d --- /dev/null +++ b/src/app.ts @@ -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; diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100644 index 0000000..3981fee --- /dev/null +++ b/src/config/database.ts @@ -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); +}); diff --git a/src/database/queries.ts b/src/database/queries.ts new file mode 100644 index 0000000..d99341b --- /dev/null +++ b/src/database/queries.ts @@ -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 { + 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 { + 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 { + 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 { + 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 }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d8c7776 --- /dev/null +++ b/src/index.ts @@ -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')); diff --git a/src/routes/licenseRoutes.ts b/src/routes/licenseRoutes.ts new file mode 100644 index 0000000..ffb7edf --- /dev/null +++ b/src/routes/licenseRoutes.ts @@ -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 { + 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' }); +} diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index c195adf..0000000 --- a/src/server.ts +++ /dev/null @@ -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); -}); diff --git a/src/services/jwtService.ts b/src/services/jwtService.ts new file mode 100644 index 0000000..a334383 --- /dev/null +++ b/src/services/jwtService.ts @@ -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); +} diff --git a/src/services/licenseService.ts b/src/services/licenseService.ts new file mode 100644 index 0000000..5f8edb3 --- /dev/null +++ b/src/services/licenseService.ts @@ -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 { + 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 { + const expiredDate = new Date(); + expiredDate.setDate(expiredDate.getDate() + plugin.trial_date); + expiredDate.setHours(23, 59, 59, 999); + return insertLicense(domain, pluginId, expiredDate, plugin.id); +} diff --git a/src/types/license.ts b/src/types/license.ts new file mode 100644 index 0000000..61534d9 --- /dev/null +++ b/src/types/license.ts @@ -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; +} diff --git a/tsconfig.json b/tsconfig.json index 5ce8c55..955552e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", + "module": "node16", + "moduleResolution": "node16", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "allowJs": true,