parent
243a0c159c
commit
bd65cdfcc2
@ -1,2 +1,25 @@
|
|||||||
# signpost
|
# Signpost
|
||||||
|
|
||||||
|
Simple fast secure Webhook handling Service. Add Flows that handle Actions-Classes
|
||||||
|
written in Javascript. Each Flow is secured by an Token. Optional you can add
|
||||||
|
an JsonSchema for the Flow to validated incoming Data.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run migrate --workspace=server
|
||||||
|
npm run start --workspace=server
|
||||||
|
```
|
||||||
|
|
||||||
|
## add flow
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run addFlow
|
||||||
|
```
|
||||||
|
|
||||||
|
### action
|
||||||
|
|
||||||
|
A Class for that have only the async method run. Each Object of class
|
||||||
|
has the current flow, the requested body and optional a JsonSchema. The Class
|
||||||
|
must be placed.
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { client, xml } from '@xmpp/client'
|
||||||
|
import debug from '@xmpp/debug'
|
||||||
|
import DOMPurify from 'isomorphic-dompurify'
|
||||||
|
|
||||||
|
import Action from './../../packages/server/actions/action.js'
|
||||||
|
import logger from './../../packages/server/helper/logger.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* using a xmpp-server to send messages in a group
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Xmpp extends Action {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async run() {
|
||||||
|
|
||||||
|
// cleaning data
|
||||||
|
this._sanitize()
|
||||||
|
|
||||||
|
const xmpp = client({
|
||||||
|
service: process.env.XMPP_SERVICE,
|
||||||
|
domain: process.env.XMPP_DOMAIN,
|
||||||
|
username: process.env.XMPP_USERNAME,
|
||||||
|
password: process.env.XMPP_PASSWORD
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.APP_DEBUG) {
|
||||||
|
debug(xmpp, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle if client has errors
|
||||||
|
xmpp.on('error', (error) => {
|
||||||
|
logger(this.flow.uuid).error('xmpp / error ' + error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle if client goes online
|
||||||
|
xmpp.once('online', async (address) => {
|
||||||
|
|
||||||
|
// join group
|
||||||
|
await xmpp.send(xml('presence', { to: process.env.XMPP_ROOM + '/' + process.env.XMPP_USERNAME }, xml('x', { xmlns: 'http://jabber.org/protocol/muc' })))
|
||||||
|
|
||||||
|
// sends a message to the room
|
||||||
|
const message = xml(
|
||||||
|
'message', {
|
||||||
|
type: 'groupchat',
|
||||||
|
to: process.env.XMPP_ROOM
|
||||||
|
},
|
||||||
|
xml('body', {}, this.data.message)
|
||||||
|
)
|
||||||
|
|
||||||
|
await xmpp.send(message)
|
||||||
|
logger(this.flow.uuid).info('xmpp / message send')
|
||||||
|
|
||||||
|
await xmpp.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
await xmpp.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clearing data from any malicous code
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_sanitize() {
|
||||||
|
this.data.message = DOMPurify.sanitize(this.data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Xmpp
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "signpost",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import fastify from 'fastify'
|
||||||
|
import getOrCreateSqlite from './db/sqlite.js'
|
||||||
|
|
||||||
|
// get config
|
||||||
|
import config from './_config.js'
|
||||||
|
config()
|
||||||
|
|
||||||
|
// create server
|
||||||
|
const server = fastify()
|
||||||
|
|
||||||
|
// add rate limit
|
||||||
|
import rateLimit from '@fastify/rate-limit'
|
||||||
|
|
||||||
|
server.register(rateLimit, {
|
||||||
|
max: process.env.APP_RATE_LIMIT_MAX,
|
||||||
|
timeWindow: process.env.APP_RATE_LIMIT_TIMEWINDOW
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
server.decorate('db', () => {
|
||||||
|
return getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* routing
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import flowHttp from './http/api/flow.js'
|
||||||
|
|
||||||
|
server
|
||||||
|
.register(flowHttp, {
|
||||||
|
'prefix': '/api/flow/v1'
|
||||||
|
})
|
||||||
|
|
||||||
|
export default server
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getting emv amd merge with basic values
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function config() {
|
||||||
|
|
||||||
|
// getting dir
|
||||||
|
const server_dir = path.resolve()
|
||||||
|
const base_dir = path.join(server_dir, '/../..')
|
||||||
|
|
||||||
|
dotenv.config({ path: base_dir + '/.env' })
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
APP_BASE_DIR: base_dir,
|
||||||
|
APP_SERVER_DIR: server_dir,
|
||||||
|
APP_SQLITE_URI_MAIN: path.join(base_dir, '/storage/main.db'),
|
||||||
|
APP_HASH_TYPE: 'sha512'
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge
|
||||||
|
dotenv.populate(process.env, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Action
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Action {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor(flow, data) {
|
||||||
|
this.flow = flow
|
||||||
|
this.data = data
|
||||||
|
|
||||||
|
if (!this.run()) {
|
||||||
|
throw new Error('run method needed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Action
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { input, password } from '@inquirer/prompts'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import path from 'path'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
|
import runMigrationSqlite from './../db/migration.js'
|
||||||
|
import getOrCreateSqlite from './../db/sqlite.js'
|
||||||
|
|
||||||
|
import TokenHelper from './../helper/token.js'
|
||||||
|
|
||||||
|
// getting flow
|
||||||
|
import FlowStore from './../store/flow.js'
|
||||||
|
|
||||||
|
// get config
|
||||||
|
import config from './../_config.js'
|
||||||
|
config()
|
||||||
|
|
||||||
|
// getting db and flowStore
|
||||||
|
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||||
|
const flowStore = new FlowStore(mainDB)
|
||||||
|
|
||||||
|
const token = crypto.randomBytes(64).toString('base64')
|
||||||
|
const flow = {
|
||||||
|
'hash' : TokenHelper.create(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
flow.uuid = uuidv4()
|
||||||
|
|
||||||
|
// adding action
|
||||||
|
flow.action = await input({
|
||||||
|
message: 'action:',
|
||||||
|
async validate(value) {
|
||||||
|
if (!value) {
|
||||||
|
return 'Required!'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// adding schema
|
||||||
|
flow.schema = await input({
|
||||||
|
message: 'schema:'
|
||||||
|
})
|
||||||
|
|
||||||
|
// adding description
|
||||||
|
flow.description = await input({
|
||||||
|
message: 'description:'
|
||||||
|
})
|
||||||
|
|
||||||
|
const id = flowStore.create(flow, token)
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
console.log(chalk.green('done'))
|
||||||
|
console.log(chalk.blue('uuid: ' + flow.uuid))
|
||||||
|
console.log(chalk.blue('token: ' + token))
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import runMigrationSqlite from './../db/migration.js'
|
||||||
|
import getOrCreateSqlite from './../db/sqlite.js'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// get config
|
||||||
|
import config from './../_config.js'
|
||||||
|
config()
|
||||||
|
|
||||||
|
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||||
|
runMigrationSqlite(path.join(path.resolve(), './../server/migration'), mainDB)
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { input, password } from '@inquirer/prompts'
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { getOrCreateSqlite } from '@nano/sqlite'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import FlowStore from './../store/flow.js'
|
||||||
|
|
||||||
|
// getting db and flowStore
|
||||||
|
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||||
|
const flowStore = new FlowStore(mainDB)
|
||||||
|
|
||||||
|
// adding action
|
||||||
|
const uuid = await input({
|
||||||
|
message: 'uuid:',
|
||||||
|
async validate(value) {
|
||||||
|
if (!value) {
|
||||||
|
return 'Required!'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
flowStore.deleteByUuid(uuid)
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { readdir, readFile } from 'node:fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* runMigrationSqlite
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @param {object} db
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function runMigrationSqlite(filePath, db) {
|
||||||
|
|
||||||
|
const SQL_EXTENSION = '.sql'
|
||||||
|
|
||||||
|
// getting files
|
||||||
|
const files = await readdir(filePath)
|
||||||
|
|
||||||
|
for (const fileName of files) {
|
||||||
|
|
||||||
|
// skip if extension not sql
|
||||||
|
if (path.extname(fileName).toLowerCase() !== SQL_EXTENSION) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = await readFile(filePath + '/' + fileName, 'utf8')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!sql.trim()) {
|
||||||
|
throw new Error(fileName + ' is empty!')
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepare(sql.trim()).run()
|
||||||
|
|
||||||
|
console.log('migrated ' + path.basename(fileName, SQL_EXTENSION))
|
||||||
|
} catch(error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default runMigrationSqlite
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* db
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Database from 'better-sqlite3'
|
||||||
|
|
||||||
|
function getOrCreateSqlite(options = {}) {
|
||||||
|
|
||||||
|
let db
|
||||||
|
|
||||||
|
options = Object.assign({
|
||||||
|
uri: process.env.NANO_SQLITE_PATH,
|
||||||
|
wal: true
|
||||||
|
}, options)
|
||||||
|
|
||||||
|
const uri = options.uri
|
||||||
|
delete options.uri
|
||||||
|
|
||||||
|
const wal = options.wal
|
||||||
|
delete options.wal
|
||||||
|
|
||||||
|
//
|
||||||
|
if (Object.keys(options).length === 0) {
|
||||||
|
db = new Database(uri)
|
||||||
|
} else {
|
||||||
|
db = new Database(uri, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wal) {
|
||||||
|
db.pragma('journal_mode = WAL')
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getOrCreateSqlite
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { resolveActionClass, resolveSchema } from './../helper/resolver.js'
|
||||||
|
import FlowStore from './../store/flow.js'
|
||||||
|
import logger from './../helper/logger.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* handle parser
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function flowHandler(request, response) {
|
||||||
|
|
||||||
|
// get single flow
|
||||||
|
const flowStore = new FlowStore(this.db())
|
||||||
|
const flow = await flowStore.findOneByUuid(request.params.uuid)
|
||||||
|
|
||||||
|
if (!flow) {
|
||||||
|
return response
|
||||||
|
.code(404)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to locals
|
||||||
|
response.locals = {
|
||||||
|
'flow' : flow
|
||||||
|
}
|
||||||
|
|
||||||
|
// getting action class
|
||||||
|
try {
|
||||||
|
response.locals.action = await import(resolveActionClass(flow.action))
|
||||||
|
} catch(error) {
|
||||||
|
logger(flow.uuid).error('flowHandler / resolve class / ' + error)
|
||||||
|
return response
|
||||||
|
.code(500)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for schema
|
||||||
|
if (flow.schema) {
|
||||||
|
try {
|
||||||
|
response.locals.schema = await import(resolveSchema(flow.schema))
|
||||||
|
} catch(error) {
|
||||||
|
logger(flow.uuid).error('flowHandler / resolve schema / ' + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default flowHandler
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import DOMPurify from 'isomorphic-dompurify'
|
||||||
|
import TokenHelper from './../helper/token.js'
|
||||||
|
import logger from './../helper/logger.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle token
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function tokenHandler(request, response) {
|
||||||
|
|
||||||
|
if (!request.headers.authorization) {
|
||||||
|
return response
|
||||||
|
.code(403)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = DOMPurify.sanitize(request.headers.authorization)
|
||||||
|
token = token.match(/^Bearer ([A-Za-z0-9._~+/-]+=*)$/)
|
||||||
|
|
||||||
|
// check if token exists
|
||||||
|
if (!token[1]) {
|
||||||
|
logger(response.locals.flow.uuid).error('token not found in header')
|
||||||
|
|
||||||
|
return response
|
||||||
|
.code(403)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if token is same as for the flow
|
||||||
|
if (!TokenHelper.equal(token[1], response.locals.flow.hash)) {
|
||||||
|
logger(response.locals.flow.uuid).error('token not equal with hash from flow')
|
||||||
|
|
||||||
|
return response
|
||||||
|
.code(403)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tokenHandler
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import pino from 'pino'
|
||||||
|
import pretty from 'pino-pretty'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mkdirSync } from "node:fs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for logger
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function logger(uuid) {
|
||||||
|
|
||||||
|
let destination = process.env.APP_BASE_DIR + '/storage/logs/'
|
||||||
|
|
||||||
|
// if name is set, adding name as directory
|
||||||
|
if (uuid) {
|
||||||
|
destination += uuid + '/'
|
||||||
|
mkdirSync(destination, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
destination += dayjs().format('DD-MM-YYYY') + '.log'
|
||||||
|
|
||||||
|
return pino({
|
||||||
|
timestamp: () => {
|
||||||
|
return `, "time":"${new Date(Date.now()).toISOString()}"`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pino.destination(destination)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default logger
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* resolve action class
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function resolveActionClass(className) {
|
||||||
|
|
||||||
|
let classPath = path.join(process.env.APP_BASE_DIR, 'resources/actions/' + className + '.js')
|
||||||
|
let result = undefined
|
||||||
|
|
||||||
|
if (fs.existsSync(classPath)) {
|
||||||
|
result = classPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Action Class ' + className + ' / ' + classPath + ' not found!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* resolve schema
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function resolveSchema(schemaName) {
|
||||||
|
|
||||||
|
let schemaPath = path.join(process.env.APP_BASE_DIR, 'resources/schemas/' + schemaName + '.json')
|
||||||
|
let result = undefined
|
||||||
|
|
||||||
|
if (fs.existsSync(schemaPath)) {
|
||||||
|
result = schemaPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Schema ' + schemaName + ' / ' + schemaPath + ' not found!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
resolveActionClass,
|
||||||
|
resolveSchema
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { createHmac, randomBytes } from 'node:crypto'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TokenHelper
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const TokenHelper = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create hash from token
|
||||||
|
*
|
||||||
|
* @param string token
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
create(token) {
|
||||||
|
const hmac = createHmac(process.env.APP_HASH_TYPE, process.env.APP_SALT)
|
||||||
|
const buffer = new Buffer(token)
|
||||||
|
|
||||||
|
return hmac.update(buffer).digest('base64')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if token and hash are equal
|
||||||
|
*
|
||||||
|
* @param string token
|
||||||
|
* @param string hash
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
equal(token, hash) {
|
||||||
|
return TokenHelper.create(token) === hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TokenHelper
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import tokenHandler from './../../handler/token.js'
|
||||||
|
import flowHandler from './../../handler/flow.js'
|
||||||
|
import logger from './../../helper/logger.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle webhook
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default async function(fastify, options) {
|
||||||
|
|
||||||
|
fastify.addHook('preHandler', flowHandler)
|
||||||
|
fastify.addHook('preHandler', tokenHandler)
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} request
|
||||||
|
* @param {object} response
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fastify.post('/:uuid(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)', async function (request, response) {
|
||||||
|
|
||||||
|
if (response.locals.schema && !request.validateInput(request.body, response.locals.schema)) {
|
||||||
|
return response
|
||||||
|
.code(400)
|
||||||
|
.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// running actions
|
||||||
|
const action = new response.locals.action.default(response.locals.flow, request.body)
|
||||||
|
|
||||||
|
response
|
||||||
|
.code(204)
|
||||||
|
.send()
|
||||||
|
|
||||||
|
// try run from action
|
||||||
|
try {
|
||||||
|
await action.run()
|
||||||
|
} catch(error) {
|
||||||
|
logger(response.locals.flow.uuid).error('webhook / run / ' + error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import server from './_bootstrap.js'
|
||||||
|
|
||||||
|
// let it rain
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
await server.listen({
|
||||||
|
port: process.env.APP_PORT
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Server: start')
|
||||||
|
console.log('Server: running on port ' + process.env.APP_PORT)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start()
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS flows (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
uuid TEXT NOT NULL UNIQUE,
|
||||||
|
hash TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
schema TEXT,
|
||||||
|
date_created_at TEXT
|
||||||
|
)
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
|
"@inquirer/prompts": "^8.3.0",
|
||||||
|
"@xmpp/client": "^0.14.0",
|
||||||
|
"better-sqlite3": "^12.6.2",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"fastify": "^5.7.4",
|
||||||
|
"isomorphic-dompurify": "^3.0.0",
|
||||||
|
"pino": "^10.3.1",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@xmpp/debug": "^0.14.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"migrate": "node cli/migrate.js",
|
||||||
|
"addFlow": "node cli/addFlow.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import Store from './store.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store for Flow
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class FlowStore extends Store {
|
||||||
|
|
||||||
|
constructor(db) {
|
||||||
|
super(db, 'flows')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
create(data, hash) {
|
||||||
|
data.date_created_at = dayjs().toISOString()
|
||||||
|
return super.create(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
findOneByUuid(uuid) {
|
||||||
|
return this._db.prepare('SELECT * FROM ' + this._tableName + ' WHERE uuid = ?')
|
||||||
|
.get(uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlowStore
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* store - abstract class
|
||||||
|
*
|
||||||
|
* @author Björn Hase <me@herr-hase.wtf>
|
||||||
|
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||||
|
* @link https://git.node001.net/HerrHase/signpost.git
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Store {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {tableName}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor(db, tableName) {
|
||||||
|
this._db = db
|
||||||
|
this._tableName = tableName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create row
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
findOneById(id) {
|
||||||
|
return this.db
|
||||||
|
.prepare('SELECT * FROM ' + this._tableName + ' WHERE id = ?')
|
||||||
|
.run(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create row
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
remove(id) {
|
||||||
|
this.db
|
||||||
|
.prepare('DELETE FROM ' + this._tableName + ' WHERE id = ?')
|
||||||
|
.run(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
update(id, data) {
|
||||||
|
return this._db.prepare('UPDATE ' + this._tableName + ' SET ' + this._prepareUpdateBinding(data) + ' WHERE id = ?')
|
||||||
|
.run(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
create(data) {
|
||||||
|
return this._db.prepare('INSERT INTO ' + this._tableName + ' (' + Object.keys(data).join(', ') + ') VALUES (' + this._prepareInsertBinding(data) + ')')
|
||||||
|
.run(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run through object and add key to string for
|
||||||
|
* binding parameters for insert
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
_prepareInsertBinding(data) {
|
||||||
|
let result = ''
|
||||||
|
let length = Object.keys(data).length
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
result += '@' + key
|
||||||
|
|
||||||
|
if (index++ < (length - 1)) {
|
||||||
|
result += ', '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run through object and add key to string for
|
||||||
|
* binding parameters for update
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
_prepareUpdateBinding(data) {
|
||||||
|
let result = ''
|
||||||
|
let length = Object.keys(data).length
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
result += key + ' = @' + key
|
||||||
|
|
||||||
|
if (index++ < (length - 1)) {
|
||||||
|
result += ', '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Store
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import {describe, it} from 'node:test'
|
||||||
|
import assert from 'node:assert'
|
||||||
|
|
||||||
|
import config from './../_config.js'
|
||||||
|
config()
|
||||||
|
|
||||||
|
import { resolveActionClass } from './../helper/resolver.js'
|
||||||
|
|
||||||
|
describe('helper / resolver', () => {
|
||||||
|
it('should get a action class', () => {
|
||||||
|
const action = resolveActionClass('test')
|
||||||
|
assert.match(action, /test.js/)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in new issue