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