First init
This commit is contained in:
commit
fa9d24a0c5
|
|
@ -0,0 +1,3 @@
|
|||
db.sqlite
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# Сервер-менеджер для ключей.
|
||||
|
||||
### Этот сервер может понадобиться в разработке безопасного api, авторизации и аутентификации.
|
||||
|
||||
|
||||
### Ключевые особенности:
|
||||
- Авторотация ключей (с возможностью указать интервал обновления)
|
||||
- Генерация ключей (пока нельзя свой вставить и навсегда)
|
||||
- Идеален для JWT и задач криптографии.
|
||||
- Плавная ротация поддерживается (старые ключи живут некоторое время после создания нового).
|
||||
- Оформлено в виде удобного api, с понятными конечными маршрутами.
|
||||
- Хранилище в виде базы данных(sqlite) с собственной оберткой.
|
||||
- AES-шифрование из коробки. Обязательно нужно обновить ключ в /keys перед продакшн-деплоем.
|
||||
- Дружелюбный язык и стэк - node js, express. Это вам не сложный java и Spring Boot.
|
||||
|
||||
|
||||
|
||||
# Краткое руководство
|
||||
### Суть
|
||||
Предполагается, что:
|
||||
|
||||
1. Сервис будет запускаться локально (извне обращения игнорироваться).
|
||||
2. Использоваться для благих целей.
|
||||
|
||||
### Как пользоваться
|
||||
Пользоваться им очень просто, как два пальца об асфальт.
|
||||
|
||||
#### Для создания ключа:
|
||||
Нужно обратиться к /reg-key с такой структурой:
|
||||
```
|
||||
POST /reg-key
|
||||
body: {
|
||||
"name": "Example",
|
||||
"update": 24
|
||||
}
|
||||
```
|
||||
|
||||
где:
|
||||
- update - интервал обновления
|
||||
- name - название ключа
|
||||
|
||||
#### Для получения ключа:
|
||||
Тут тоже все просто, отличие лишь в способе передачи информации:
|
||||
```
|
||||
GET /get-key
|
||||
query-параметры:
|
||||
- name
|
||||
- kid (опционально)
|
||||
|
||||
примеры:
|
||||
- http://localhost/get-key?name=Example
|
||||
- http://localhost/get-key?name=Example&kid=4adc0149-49e7-45e7-b0af-5e0c4da5fe3f
|
||||
```
|
||||
Все, это все что вам нужно знать, чтобы использовать этот сервис.
|
||||
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>bؤU<EFBFBD>|م 7خخعy<79><7F>H@<40>د<EFBFBD>uية<D98A><D8A9><EFBFBD>ل#<23>I
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.2.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { Worker } from 'worker_threads'
|
||||
import { writeFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { unlink } from 'fs/promises'
|
||||
import { getClassMethodNames } from '../util.js'
|
||||
import { Table } from './Table.js'
|
||||
|
||||
export class AsyncTableWrapper {
|
||||
static _sharedWorker = null
|
||||
static _sharedCallbacks = new Map()
|
||||
static _messageId = 0
|
||||
static _initOnce(dbFile, tableConfigs) {
|
||||
if (this._sharedWorker) return
|
||||
|
||||
const workerScript = `
|
||||
import { parentPort, workerData } from 'worker_threads'
|
||||
import { Database } from './lib/db/Database.js'
|
||||
import { Table } from './lib/db/Table.js'
|
||||
|
||||
const db = new Database(workerData.dbName)
|
||||
const tables = new Map()
|
||||
|
||||
for (const config of workerData.tableConfigs) {
|
||||
tables.set(config.name, new Table(db, config))
|
||||
}
|
||||
|
||||
parentPort.on('message', async ({ id, tableName, method, args }) => {
|
||||
const table = tables.get(tableName)
|
||||
if (!table) {
|
||||
return parentPort.postMessage({ id, error: 'Table not found: ' + tableName })
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await table[method](...args)
|
||||
parentPort.postMessage({ id, result })
|
||||
} catch (err) {
|
||||
parentPort.postMessage({ id, error: err.message })
|
||||
}
|
||||
})
|
||||
`
|
||||
|
||||
this._filename = path.resolve('./src/.tmp-worker-shared.mjs')
|
||||
writeFileSync(this._filename, workerScript)
|
||||
|
||||
this._sharedWorker = new Worker(this._filename, {
|
||||
type: 'module',
|
||||
workerData: {
|
||||
dbName: dbFile,
|
||||
tableConfigs: tableConfigs
|
||||
}
|
||||
})
|
||||
|
||||
this._sharedWorker.on('message', ({ id, result, error }) => {
|
||||
const cb = this._sharedCallbacks.get(id)
|
||||
if (!cb) return
|
||||
const { resolve, reject } = cb
|
||||
this._sharedCallbacks.delete(id)
|
||||
if (error) reject(new Error(error))
|
||||
else resolve(result)
|
||||
})
|
||||
}
|
||||
|
||||
static async terminate() {
|
||||
if (this._sharedWorker) {
|
||||
await unlink(this._filename)
|
||||
await this._sharedWorker.terminate()
|
||||
this._sharedWorker = null
|
||||
}
|
||||
}
|
||||
|
||||
constructor(tableName) {
|
||||
this.tableName = tableName
|
||||
|
||||
for (const methodName of getClassMethodNames(Table)) {
|
||||
this[methodName] = async (...args) => {
|
||||
return AsyncTableWrapper._call(this.tableName, methodName, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static _call(tableName, method, args) {
|
||||
const id = ++this._messageId
|
||||
return new Promise((resolve, reject) => {
|
||||
this._sharedCallbacks.set(id, { resolve, reject })
|
||||
this._sharedWorker.postMessage({ id, tableName, method, args })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import openDB from 'better-sqlite3'
|
||||
|
||||
export class Database {
|
||||
|
||||
/**
|
||||
* @param {string} filename Имя файла базы данных
|
||||
*/
|
||||
constructor (filename) {
|
||||
this.filename = filename
|
||||
this.db = openDB(`db/${filename}.sqlite`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} tableConfig
|
||||
* @returns {boolean}
|
||||
*/
|
||||
createTable (tableConfig) {
|
||||
try {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ${tableConfig.name} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
${Object.entries(tableConfig.columns).map(
|
||||
([name, type]) => name + ' ' + type).join(', ')
|
||||
}
|
||||
)
|
||||
`)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error("Проблема при создании таблицы :", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tableName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
deleteTable(tableName) {
|
||||
try {
|
||||
this.db.exec(`DROP TABLE IF EXISTS ${tableName}`)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error("Проблема при удалении таблицы :", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import { Database } from "./Database.js"
|
||||
|
||||
function sanitizeValue(value) {
|
||||
if (value === undefined) return null
|
||||
if (typeof value === 'boolean') return value ? 1 : 0
|
||||
if (typeof value === 'object' && value !== null) return JSON.stringify(value)
|
||||
return value
|
||||
}
|
||||
|
||||
export class Table {
|
||||
/**
|
||||
* @param {Database} db
|
||||
* @param {Object} tableConfig
|
||||
*/
|
||||
constructor(db, tableConfig) {
|
||||
this.database = db
|
||||
this.tableName = tableConfig.name
|
||||
this.columns = Object.entries(tableConfig.columns).map(([name, _]) => name)
|
||||
|
||||
db.createTable(tableConfig)
|
||||
|
||||
// Подготовленные запросы
|
||||
this.getByIdStmt = db.db.prepare(`SELECT * FROM ${this.tableName} WHERE id = ?`)
|
||||
this.getAllStmt = db.db.prepare(`SELECT * FROM ${this.tableName}`)
|
||||
this.delByIdStmt = db.db.prepare(`DELETE FROM ${this.tableName} WHERE id = ?`)
|
||||
|
||||
this.addStmt = db.db.prepare(
|
||||
`INSERT INTO ${this.tableName} (${this.columns.join(', ')}) VALUES (${Array(this.columns.length).fill('?').join(', ')})`
|
||||
)
|
||||
|
||||
const setColumns = this.columns.filter(col => col !== 'id').map(col => `${col} = ?`).join(', ')
|
||||
this.updateByIdStmt = db.db.prepare(`UPDATE ${this.tableName} SET ${setColumns} WHERE id = ?`)
|
||||
|
||||
// Для динамических запросов по полям
|
||||
this.getByFieldStmts = new Map()
|
||||
this.getAllByFieldStmts = new Map()
|
||||
}
|
||||
|
||||
// Получение по id
|
||||
getById(id) {
|
||||
try {
|
||||
return this.getByIdStmt.get(id)
|
||||
} catch (e) {
|
||||
console.error("Ошибка при getById:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Получение всех записей
|
||||
getAll() {
|
||||
try {
|
||||
return this.getAllStmt.all()
|
||||
} catch (e) {
|
||||
console.error("Ошибка при getAll:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление одной записи
|
||||
add(data) {
|
||||
try {
|
||||
if (Array.isArray(data)) {
|
||||
const sanitized = data.map(sanitizeValue)
|
||||
this.addStmt.run(sanitized)
|
||||
return true
|
||||
} else if (typeof data === 'object' && data) {
|
||||
const dataArr = this.columns.map(key => sanitizeValue(data[key]))
|
||||
this.addStmt.run(dataArr)
|
||||
return true
|
||||
}
|
||||
throw new Error("Некорректный аргумент для add")
|
||||
} catch (e) {
|
||||
console.error("Ошибка при вставке:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление многих записей в транзакции
|
||||
addMany(dataArr) {
|
||||
this.database.db.exec('BEGIN TRANSACTION')
|
||||
try {
|
||||
for (const data of dataArr) {
|
||||
if (Array.isArray(data)) {
|
||||
const sanitized = data.map(sanitizeValue)
|
||||
this.addStmt.run(sanitized)
|
||||
} else if (typeof data === 'object' && data) {
|
||||
const arr = this.columns.map(key => sanitizeValue(data[key]))
|
||||
this.addStmt.run(arr)
|
||||
} else {
|
||||
throw new Error("Некорректный аргумент для addMany")
|
||||
}
|
||||
}
|
||||
this.database.db.exec('COMMIT')
|
||||
return true
|
||||
} catch (e) {
|
||||
this.database.db.exec('ROLLBACK')
|
||||
console.error('Ошибка при множественной вставке:', e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление по id
|
||||
updateById(id, data) {
|
||||
try {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new Error("Некорректный аргумент для updateById")
|
||||
}
|
||||
const updateColumns = this.columns.filter(col => col !== 'id')
|
||||
const values = updateColumns.map(col => sanitizeValue(data[col]))
|
||||
values.push(id)
|
||||
this.updateByIdStmt.run(values)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error("Ошибка при updateById:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Множественное обновление по id в транзакции
|
||||
updateManyById(dataArr) {
|
||||
this.database.db.exec('BEGIN TRANSACTION')
|
||||
try {
|
||||
for (const data of dataArr) {
|
||||
if (!data || typeof data !== 'object' || data.id === undefined) {
|
||||
throw new Error("Каждый объект должен содержать id для updateManyById")
|
||||
}
|
||||
this.updateById(data.id, data)
|
||||
}
|
||||
this.database.db.exec('COMMIT')
|
||||
return true
|
||||
} catch (e) {
|
||||
this.database.db.exec('ROLLBACK')
|
||||
console.error("Ошибка при множественном обновлении:", e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление по id
|
||||
delById(id) {
|
||||
try {
|
||||
this.delByIdStmt.run(id)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error("Ошибка при delById:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Множественное удаление по id в транзакции
|
||||
delManyByIds(ids) {
|
||||
this.database.db.exec('BEGIN TRANSACTION')
|
||||
try {
|
||||
for (const id of ids) {
|
||||
try {
|
||||
this.delByIdStmt.run(id)
|
||||
} catch (e) {
|
||||
console.error('Ошибка при удалении id:', id, e.message)
|
||||
}
|
||||
}
|
||||
this.database.db.exec('COMMIT')
|
||||
return true
|
||||
} catch (e) {
|
||||
this.database.db.exec('ROLLBACK')
|
||||
console.error('Ошибка при множественном удалении:', e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Получение одной записи по полю
|
||||
getBy(field, value) {
|
||||
try {
|
||||
if (!this.getByFieldStmts.has(field)) {
|
||||
const stmt = this.database.db.prepare(
|
||||
`SELECT * FROM ${this.tableName} WHERE ${field} = ? LIMIT 1`
|
||||
)
|
||||
this.getByFieldStmts.set(field, stmt)
|
||||
}
|
||||
const stmt = this.getByFieldStmts.get(field)
|
||||
return stmt.get(value)
|
||||
} catch (e) {
|
||||
console.error(`Ошибка при getBy(${field}):`, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Получение всех записей по полю
|
||||
getAllBy(field, value) {
|
||||
try {
|
||||
if (!this.getAllByFieldStmts.has(field)) {
|
||||
const stmt = this.database.db.prepare(
|
||||
`SELECT * FROM ${this.tableName} WHERE ${field} = ?`
|
||||
)
|
||||
this.getAllByFieldStmts.set(field, stmt)
|
||||
}
|
||||
const stmt = this.getAllByFieldStmts.get(field)
|
||||
return stmt.all(value)
|
||||
} catch (e) {
|
||||
console.error(`Ошибка при getAllBy(${field}):`, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Получение по множеству условий (AND)
|
||||
getByFields(fieldsObj) {
|
||||
try {
|
||||
const keys = Object.keys(fieldsObj)
|
||||
if (keys.length === 0) throw new Error("Пустой объект условий")
|
||||
|
||||
const whereClause = keys.map(k => `${k} = ?`).join(' AND ')
|
||||
const values = keys.map(k => fieldsObj[k])
|
||||
|
||||
const stmt = this.database.db.prepare(
|
||||
`SELECT * FROM ${this.tableName} WHERE ${whereClause}`
|
||||
)
|
||||
return stmt.all(values)
|
||||
} catch (e) {
|
||||
console.error("Ошибка при getByFields:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Получить первую запись (по id ASC)
|
||||
getFirst() {
|
||||
try {
|
||||
const stmt = this.database.db.prepare(
|
||||
`SELECT * FROM ${this.tableName} ORDER BY id ASC LIMIT 1`
|
||||
)
|
||||
return stmt.get()
|
||||
} catch (e) {
|
||||
console.error("Ошибка при getFirst:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Получить последнюю запись (по id DESC)
|
||||
getLast() {
|
||||
try {
|
||||
const stmt = this.database.db.prepare(
|
||||
`SELECT * FROM ${this.tableName} ORDER BY id DESC LIMIT 1`
|
||||
)
|
||||
return stmt.get()
|
||||
} catch (e) {
|
||||
console.error("Ошибка при getLast:", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { generateKeyPairSync } from 'crypto';
|
||||
|
||||
export function genRsaKey(size = 2048) {
|
||||
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: size,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
});
|
||||
return {privateKey, publicKey}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import crypto from 'crypto';
|
||||
|
||||
export function encrypt(text, key) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return Buffer.concat([iv, tag, encrypted]).toString('base64');
|
||||
}
|
||||
|
||||
export function decrypt(data, key) {
|
||||
const bData = Buffer.from(data, 'base64');
|
||||
const iv = bData.slice(0, 16);
|
||||
const tag = bData.slice(16, 32);
|
||||
const text = bData.slice(32);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const decrypted = decipher.update(text) + decipher.final('utf8');
|
||||
return decrypted;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export function getClassMethodNames(klass) {
|
||||
return Object.getOwnPropertyNames(klass.prototype)
|
||||
.filter(name => typeof klass.prototype[name] === 'function' && name !== 'constructor')
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import fs from 'fs';
|
||||
import express from 'express'
|
||||
import { AsyncTableWrapper } from "./lib/db/AsyncTableWrap.js"
|
||||
import { encrypt, decrypt } from './lib/sec.js';
|
||||
import { genRsaKey } from './lib/genKey.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import cron from 'node-cron'
|
||||
|
||||
const KEY = fs.readFileSync('./keys/aes-256.key')
|
||||
const PORT = 39391
|
||||
const TABLE_CONFIG = [
|
||||
{
|
||||
name: "rsa_keys",
|
||||
columns: {
|
||||
name: "varchar(64)",
|
||||
kid: "varchar(64)",
|
||||
publicKey: "text",
|
||||
privateKey: "text",
|
||||
actual: "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "keys",
|
||||
columns: {
|
||||
name: "varchar(64)",
|
||||
updateInterval: "integer" //в часах
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
AsyncTableWrapper._initOnce('db', TABLE_CONFIG)
|
||||
|
||||
const app = express()
|
||||
let keyTable, rsaTable
|
||||
|
||||
async function updateKey (name) {
|
||||
|
||||
const keys = await rsaTable.getAllBy("name", name)
|
||||
|
||||
if(keys.length >= 1) {
|
||||
const key = keys.at(-1)
|
||||
key.actual = false
|
||||
await rsaTable.updateById(key.id, key)
|
||||
}
|
||||
|
||||
const {privateKey, publicKey} = genRsaKey()
|
||||
const kid = randomUUID()
|
||||
|
||||
console.log("Обновение ключа \"" + name + "\" c kid: " + kid)
|
||||
|
||||
await rsaTable.add({
|
||||
name,
|
||||
kid,
|
||||
publicKey: encrypt(publicKey, KEY),
|
||||
privateKey: encrypt(privateKey, KEY),
|
||||
actual: true
|
||||
})
|
||||
}
|
||||
|
||||
function autoUpdate (name, update) {
|
||||
cron.schedule(`0 */${update} * * *`, () => updateKey(name))
|
||||
// cron.schedule(`*/1 * * * *`, () => updateKey(name))
|
||||
}
|
||||
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
app.post("/reg-key", async (req, res) => {
|
||||
const {name, update} = req.body
|
||||
|
||||
if (!name || !update || isNaN(Number(update))) {
|
||||
return res.status(400).json({ ошибка: "Неверные параметры" });
|
||||
}
|
||||
|
||||
const record = await keyTable.getBy("name", name)
|
||||
|
||||
if(record)
|
||||
return res.status(409).json({ошибка: "Запись существует"})
|
||||
|
||||
await keyTable.add({name, updateInterval: update})
|
||||
updateKey(name)
|
||||
|
||||
autoUpdate(name, update)
|
||||
|
||||
return res.json({статус: "Запись добавлена"})
|
||||
})
|
||||
|
||||
app.get("/get-key", async (req, res) => {
|
||||
const {name, kid = null} = req.query
|
||||
|
||||
if(!(await keyTable.getBy("name", name)))
|
||||
return res.status(401).json({ошибка: "Запись не найдена"})
|
||||
|
||||
if(kid) {
|
||||
const key = await rsaTable.getBy("kid", kid)
|
||||
if(key)
|
||||
return res.json({
|
||||
publicKey: decrypt(key.publicKey, KEY),
|
||||
privateKey: decrypt(key.privateKey, KEY)
|
||||
})
|
||||
return res.status(401).json({ошибка: "Запись не найдена по kid"})
|
||||
}
|
||||
|
||||
const key = (await rsaTable.getAllBy("name", name)).at(-1)
|
||||
|
||||
return res.json({
|
||||
publicKey: decrypt(key.publicKey, KEY),
|
||||
privateKey: decrypt(key.privateKey, KEY)
|
||||
})
|
||||
})
|
||||
|
||||
async function recaveryCron () {
|
||||
console.log("Восстановление расписаний")
|
||||
const keys = await keyTable.getAll()
|
||||
keys.forEach(key => {
|
||||
autoUpdate(key.name, key.updateInterval)
|
||||
console.log("Запись \"" + key.name + "\" возобновлена")
|
||||
});
|
||||
}
|
||||
|
||||
const server = app.listen(PORT, async error => {
|
||||
rsaTable = new AsyncTableWrapper('rsa_keys')
|
||||
keyTable = new AsyncTableWrapper('keys')
|
||||
console.log("Сервер стартовал")
|
||||
|
||||
recaveryCron()
|
||||
|
||||
if (error) console.log("Error: " + error)
|
||||
})
|
||||
|
||||
|
||||
|
||||
const shutdown = async () => {
|
||||
console.log('Завершение работы...')
|
||||
|
||||
await AsyncTableWrapper.terminate()
|
||||
.then(() => {
|
||||
console.log("Закрытие соединения с бд")
|
||||
})
|
||||
|
||||
server.close(() => {
|
||||
console.log('Cервер остановлен')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
console.error('Принудительное завершение...')
|
||||
process.exit(1)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('uncaughtException', err => {
|
||||
console.error('Непойманное исключение:', err)
|
||||
shutdown()
|
||||
})
|
||||
Loading…
Reference in New Issue