before frontend login

This commit is contained in:
peter
2019-11-06 11:15:00 +01:00
parent 9d6681843b
commit 5466a3e8e8
57 changed files with 1013 additions and 2085 deletions
@@ -3,52 +3,26 @@
import { getLogger } from 'frontblock-generic/Types';
import { promises as fs, mkdirSync } from "fs"
import { RPCServer } from 'rpclibrary'
import { AdminConf, TableDefiniton } from './Types';
import { RPCConfigLoader } from './RPCConfigLoader';
import { RPCPluginLoader } from './PluginLoader';
import { AdminConf, TableDefiniton } from '../Types/Types';
import { RPCConfigLoader } from '../Components/RPCConfigLoader';
import * as Path from 'path'
import Knex = require('knex');
import http = require('http');
import express = require('express');
import { TableDefinitionExporter } from './Interfaces';
import { FrontworkEventBus } from './Eventbus';
import * as Knex from 'knex';
import * as http from 'http';
import * as express from 'express';
import { TableDefinitionExporter } from '../Types/Interfaces';
import { FrontworkComponent } from '../Types/FrontworkComponent';
const logger = getLogger("admin", 'debug')
export class FrontworkAdmin
implements TableDefinitionExporter {
knex:Knex
config: RPCConfigLoader<AdminConf>
private express
private httpServer
private pluginLoader:RPCPluginLoader = new RPCPluginLoader(this)
private eventBus:FrontworkEventBus = new FrontworkEventBus(this)
config: RPCConfigLoader<AdminConf>
knex:Knex
constructor(){}
async start(){
this.initConfig()
await this.makeKnex()
this.startWebsocket()
this.startWebserver()
}
protected configChangeHandler = (conf:AdminConf, key?:string) => {
if(key === 'dbConf'){
this.makeKnex()
}
}
getConfigKey(key:string){
return this.config.getConfigKey(key)
}
setConfigKey(key:string, value:any){
return this.config.setConfigKey(key, value)
}
private initConfig(){
constructor(private components: FrontworkComponent[]){
this.config = new RPCConfigLoader<AdminConf>({
name: "FrontworkAdminConf",
getDefaultConfig: () => {
@@ -65,20 +39,44 @@ implements TableDefinitionExporter {
}
}
}, './config', this.configChangeHandler)
components.forEach(c => {
c.admin = this
if(c.onSetAdmin) c.onSetAdmin(this)
})
}
async start(){
await this.makeKnex()
this.startWebsocket()
await Promise.all( this.components.map(c => c.initialize?c.initialize():undefined ))
this.startWebserver()
}
protected configChangeHandler = (conf:AdminConf, key?:string) => {
if(key === 'dbConf'){
this.makeKnex()
}
}
getConfigKey(key:string){
return this.config.getConfigKey(key)
}
setConfigKey(key:string, value:any){
return this.config.setConfigKey(key, value)
}
getTableDefinitions(): TableDefiniton[]{
return [
this.eventBus,
...this.pluginLoader.getPlugins()
...this.components
].flatMap(exporter => exporter.getTableDefinitions())
}
private startWebsocket(){
new RPCServer(20000, [
this.config,
this.pluginLoader,
this.eventBus
...this.components,
])
}
@@ -0,0 +1,34 @@
import { RPCExporter } from "rpclibrary"
import { FrontworkAdmin } from "../../Admin/Admin"
import { TableDefiniton } from "../../Types/Types"
import { PrivilegedRPCExporter } from "../../Types/PrivilegedRPCExporter"
import { FrontworkComponent } from "../../Types/FrontworkComponent"
export class Debugger
implements FrontworkComponent<any,any>{
public admin: FrontworkAdmin
name = "Debugger"
constructor(private exporters: PrivilegedRPCExporter[]){
}
exportRPCs(){
return [{
name: 'getTable',
call: async(table:string) => this.admin.knex.select('*').from(table)
},
...this.exporters.flatMap(e => e.exportRPCs()),
...this.exporters.flatMap(e => e.exportRPCFeatures().flatMap(e => e.exportRPCs()))]
}
exportRPCFeatures(): RPCExporter<any, any, {}>[] {
return []
}
getTableDefinitions(): TableDefiniton[] {
return []
}
}
@@ -1,9 +1,10 @@
import { RPCExporter, SubscriptionResponse, ErrorResponse, SuccessResponse } from "rpclibrary";
import { FrontworkAdmin } from "./Admin";
import { TableDefinitionExporter } from "./Interfaces";
import { FrontworkAdmin } from "../Admin/Admin";
import { TableDefinitionExporter } from "../Types/Interfaces";
import { getLogger } from 'frontblock-generic/Types';
import * as uuid from 'uuid/v4'
import { TableDefiniton } from "../Types/Types";
export type NotificationSeverity = 'Info' | 'Important' | 'Error'
@@ -84,11 +85,11 @@ implements RPCExporter<EventbusIfc, "Eventbus">, TableDefinitionExporter {
}]
}
getTableDefinitions(){
getTableDefinitions(): TableDefiniton[]{
return [{
name: 'notifications',
tableBuilder: (table) => {
table.increments('ID');
table.increments('ID').primary();
table.string('severity');
table.string('topic');
table.string('message');
+107
View File
@@ -0,0 +1,107 @@
import { TableDefinitionExporter } from "../../Types/Interfaces";
import { TableDefiniton, _Rank } from "../../Types/Types";
import { FrontworkAdmin } from "../../Admin/Admin";
import { T1 } from "../../Types/Items";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
import { RPC } from "rpclibrary";
const fetch = require('node-fetch')
const xml2js = require('xml2js');
const parser = new xml2js.Parser(/* options */);
export type ItemManagerFeatureIfc = any
export type Item = {
id?:number
name:string
iconname:string
url:string
quality:string
hidden:boolean
}
export class ItemManager
implements FrontworkComponent<ItemManagerFeatureIfc>, TableDefinitionExporter{
admin:FrontworkAdmin
name = "ItemManager";
exportRPCs(): RPC<any, any>[]{
return [{
name: 'getItems',
call: async () => await this.getItems()
},{
name: 'getItem',
call: async (name:string) => await this.getItem(name)
}]
}
exportRPCFeatures() {
return []
}
getItems = async () :Promise<Item[]> => await this.admin.knex.select('*').from('items')
getItem = async (name:string):Promise<Item> => {
const res = await fetch('https://classic.wowhead.com/item='+name+'&xml');
const txt = await res.text();
const r = await parser.parseStringPromise(txt);
try{
return <Item>{
name: r.wowhead.item[0].name[0],
iconname: r.wowhead.item[0].icon[0]._,
url: r.wowhead.item[0].link[0],
quality: r.wowhead.item[0].quality[0]._,
}
}catch (e){
console.log(name)
throw e
}
}
getTableDefinitions(): TableDefiniton[] {
return [
{
name: 'reservations',
tableBuilder: (table) => {
table.integer("user_id").primary()
table.foreign("user_id").references("id").inTable('users')
table.integer("item_id").primary()
table.foreign("item_id").references("id").inTable('items')
table.integer("raid_id").primary()
table.foreign("raid_id").references("id").inTable('raids')
}
},{
name: 'items',
tableBuilder: (table) => {
table.increments("id").primary()
table.string('name').unique().notNullable()
table.string('iconname').notNullable()
table.string('url').notNullable()
table.string('quality').defaultTo('Epic').notNullable()
table.boolean('hidden').defaultTo(false).notNullable()
}
}]
}
countItems = async() :Promise<number> => {
const count = await this.admin.knex('items').count('*');
return <number>count[0]['count(*)']
}
initialize = async() => {
const allItems = [...T1]
const countCache = await this.countItems()
if(countCache != allItems.length){
const items:Item[] = await Promise.all(allItems.map(async (i) => this.getItem(i)))
try{
await this.admin
.knex('items')
.insert(items)
}catch(e){
console.error(e)
}
}
}
}
@@ -0,0 +1,196 @@
import { RPCServer } from "rpclibrary";
import { TableDefiniton, AnyRPCExporter, User, RPCPermission, _Rank, Token, Auth } from "../../Types/Types";
import { FrontworkAdmin } from "../../Admin/Admin";
import { PrivilegedRPCExporter } from "../../Types/PrivilegedRPCExporter";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
const uuid = require('uuid/v4')
export class LoginManager
implements FrontworkComponent{
name = "Authenticator" as "Authenticator";
admin:FrontworkAdmin
constructor(private exporters: PrivilegedRPCExporter[]){}
exportRPCs() {
return [
{
name: 'login' as 'login',
call: async (username:string, pwHash:string) => await this.login(username, pwHash)
},{
name: 'authenticate' as 'authenticate',
call: async (tokenValue:string | Token) => await this.authenticate(tokenValue)
}
]
}
onSetAdmin(admin:FrontworkAdmin){
this.exporters.forEach(e => e['admin'] = admin)
}
exportRPCFeatures() {
return [{
name: 'createUser' as 'createUser',
exportRPCs: () => [{
name: 'createUser' as 'createUser',
call: async (user:User) => {
return await this.createUser(user)
}
}]
},{
name: 'modifyPermissions' as 'modifyPermissions',
exportRPCs: () => [{
name: 'getPermissions' as 'getPermissions',
call: async () => await this.getPermissions()
},{
name: 'setPermission' as 'setPermission',
call: async (perm: RPCPermission) => await this.setPermission(perm)
}]
}]
}
getTableDefinitions(): TableDefiniton[] {
return [
{
name: 'users',
tableBuilder: (table) => {
table.increments("id").primary()
table.string("name").notNullable().unique()
table.string("pwhash").notNullable()
table.string("rank").notNullable()
table.string("class").notNullable()
table.string("email").nullable().unique()
}
},{
name: 'rpcpermissions',
tableBuilder: (table) => {
table.string("rpcName").primary().notNullable()
table.boolean("ADMIN").defaultTo(true).notNullable()
_Rank.forEach(r => table.boolean(r).defaultTo(false).notNullable())
}
},{
name: 'tokens',
tableBuilder: (table) => {
table.string('value').primary()
table.integer('user_id').notNullable()
table.foreign('user_id').references('users')
table.dateTime('created').defaultTo(this.admin.knex.fn.now())
}
}
,...this.exporters.flatMap(exp => exp['getTableDefinitions']?exp['getTableDefinitions']():undefined)]
}
async initialize(){
await Promise.all(
[this, ...this.exporters].flatMap(exp => exp.exportRPCFeatures().map(async (rpc) => {
try{
await this.admin.knex.insert({ rpcname: rpc.name }).into('rpcpermissions')
}catch(e){}
})))
await Promise.all(this.exporters.map(ex => ex['initialize']?ex['initialize']():undefined))
}
async setPermission(permission: RPCPermission){
await this.admin.knex('rpcpermissions')
.where('rpcname', '=', permission.name)
.update(permission)
}
getPermissions = async () : Promise<RPCPermission[]> => {
return await this.admin.knex.select('*').from('rpcpermissions')
}
getRPCForUser = async (user:User): Promise<AnyRPCExporter[]> => {
return [...this.exportRPCFeatures(), ...this.exporters.flatMap((exp) => exp.exportRPCFeatures())]
}
createUser = async(user:User): Promise<User> => {
await this.admin.knex('users')
.insert(user)
const users = await this.admin.knex
.select("*")
.from('users')
.where(user)
return users[0]
}
login = async(username:string, pwHash:string) : Promise<Token> => {
const res:User[] = await this.admin.knex
.select("*")
.from('users')
.where({ name: username })
if(res.length > 0 && pwHash === res[0].pwhash){
return await this.createToken(res[0])
}
throw new Error('login failed')
}
authenticate = async(tokenValue: string | Token) : Promise<Auth> => {
if(typeof tokenValue !== 'string') tokenValue = tokenValue.value
const res : User[] = await this.admin.knex
.select('users.id', 'name', 'class', 'rank', 'email')
.from('tokens')
.join('users', function(){
this.on('users.id', '=', 'tokens.user_id')
})
.where({ value: tokenValue})
if(res.length === 0)
throw new Error('authentication failed')
const allowedRPCs = await this.getRPCForUser(res[0])
const randomPort = 20000 + Math.floor(Math.random() * 10000)
while(true){
try{
let commSock = new RPCServer(randomPort, allowedRPCs, {
closeHandler: () => {
console.log(res[0].name, 'disconnected')
commSock.destroy()
},
connectionHandler: () => {
console.log(res[0].name, 'connected')
cancelTimeout()
},
sesame: tokenValue
})
const timeout = setTimeout(() => {
console.log(res[0].name, 'timeout')
commSock.destroy()
}, 10000)
const cancelTimeout = () => { clearTimeout(timeout) }
break;
}catch(e){
//retry
}
}
return {
port: randomPort,
user: res[0],
token: {
value: tokenValue,
user_id: res[0].id!
}
}
}
createToken = async(user:User): Promise<Token> => {
const old = await this.admin.knex.select('*').from('tokens').where({user_id: user.id})
if(old.length === 0){
const token:Token = {
value: uuid(),
user_id: user.id!
}
await this.admin.knex('tokens').insert(token)
return token
}else{
return old[0]
}
}
}
@@ -1,8 +1,8 @@
import { RPCExporter } from "rpclibrary";
import { Git } from "upgiter"
import { FolderStatus } from "upgiter/js/src/Types";
import { Plugin } from "./Plugin";
import { FrontworkAdmin } from "./Admin";
import { Plugin } from "../Types/Plugin";
import { FrontworkAdmin } from "../Admin/Admin";
class PluginLoader {
private runningPlugins: Plugin[] = []
@@ -0,0 +1,95 @@
import { TableDefiniton, User, _Rank, Raid, Signup, RaidManagerFeatureIfc } from "../../Types/Types";
import { FrontworkAdmin } from "../../Admin/Admin";
import { FrontworkComponent } from "../../Types/FrontworkComponent";
export class RaidManager
implements FrontworkComponent<RaidManagerFeatureIfc>{
name = "RaidManager";
admin: FrontworkAdmin
exportRPCs = () => []
exportRPCFeatures() {
return [{
name: 'manageRaid' as 'manageRaid',
exportRPCs: () => [{
name: 'createRaid' as 'createRaid',
call: async (raid:Raid) => await this.createRaid(raid)
},{
name: 'addSignup' as 'addSignup',
call: async(signup: Signup) => await this.addSignup(signup)
},{
name: 'removeSignup' as 'removeSignup',
call: async(signup: Signup) => await this.removeSignup(signup)
}]
},{
name: 'signup' as 'signup',
exportRPCs: () => [{
name: 'getRaids' as 'getRaids',
call: async () => await this.getRaids()
},{
name: 'getSingups' as 'getSingups',
call: async(raid: Raid) => await this.getSignups(raid)
},{
name: 'sign' as 'sign',
call: async(user:User, raid:Raid) => await this.sign(user, raid)
}]
},]
}
getTableDefinitions(): TableDefiniton[] {
return [
{
name: 'raids',
tableBuilder: (table) => {
table.increments('id').primary()
table.dateTime('start').notNullable()
table.string('description').notNullable()
table.string('title').notNullable()
table.integer('minrank').notNullable()
}
},{
name: 'signups',
tableBuilder: (table) => {
table.integer('raid_id').primary()
table.foreign('raid_id').references('id').inTable('raids')
table.integer('user_id').primary()
table.foreign('user_id').references('id').inTable('users')
}
}
]
}
createRaid = async (raid:Raid) => await this.admin
.knex('raids')
.insert(raid)
addSignup = async (signup: Signup) => await this.admin
.knex('signups')
.insert(signup)
removeSignup = async (signup: Signup) => await this.admin
.knex('signups')
.where({
raid_id: signup.raid_id,
user_id: signup.user_id
})
.delete()
getRaids = async () => await this.admin.knex
.select('*')
.from('raids')
getSignups = async (raid:Raid) => await this.admin.knex
.select('*')
.from('signups')
.where('raid_id', '=', raid.id!)
sign = async (user:User, raid:Raid) => await this.admin
.knex('signups')
.insert({
raid_id: raid.id!,
user_id: user.id!
})
}
-24
View File
@@ -1,24 +0,0 @@
import { Plugin } from "./Plugin"
import { getLogger } from "frontblock-generic/Types"
import { FrontworkAdmin } from "./Admin";
var exec = require('child-process-promise').exec;
const logger = getLogger("installer", 'info')
export type NPMPkgName = string
export type NPMVersion = string
export const installAdmin = (plugins: Plugin[] = []) => {
const npmPkgs:[NPMPkgName, NPMVersion][] = [['sqlite3', '4.1.0'], ['knex', '0.19.2']]
const deps = npmPkgs.map(tuple => tuple.join('@') ).join(" ")
logger.info("Installing plaform dependencies: "+deps)
exec("npm i " + deps).then(async process => {
logger.debug(process.stdout)
const Admin = require("./Admin").FrontworkAdmin
const fbAdmin:FrontworkAdmin = new Admin(plugins)
fbAdmin.start()
})
}
+20 -2
View File
@@ -1,2 +1,20 @@
import { installAdmin } from "./Installer";
installAdmin()
import { FrontworkAdmin } from './Admin/Admin'
import { RaidManager } from "./Components/Raid/RaidManager";
import { ItemManager } from "./Components/Item/ItemManager";
import { LoginManager } from "./Components/Login/LoginManager";
import { Debugger } from './Components/Debugger/Debugger';
import { FrontworkComponent } from './Types/FrontworkComponent';
require('events').EventEmitter.defaultMaxListeners = 0;
let raidManager = new RaidManager()
let itemManager = new ItemManager()
let loginManager = new LoginManager([
raidManager,
itemManager
])
let components:FrontworkComponent[] = [ raidManager, itemManager, loginManager ]
let dbg = new Debugger(components)
new FrontworkAdmin([dbg, ...components]).start()
-14
View File
@@ -1,14 +0,0 @@
import Knex = require("knex")
export declare type NotificationSeverity = 'Info' | 'Important' | 'Error';
export type AdminConf = {
httpPort: number,
dbConf:Knex.Config,
eventBusConf: { [topic in string]: NotificationSeverity}
}
export type TableDefiniton = {
name: string,
tableBuilder: (table: Knex.CreateTableBuilder) => void
}
+24
View File
@@ -0,0 +1,24 @@
import { PrivilegedRPCExporter } from "./PrivilegedRPCExporter";
import { RPCInterface, RPC, AnyFunction, RPCExporter } from "rpclibrary";
import { TableDefinitionExporter } from "./Interfaces";
import { FrontworkAdmin } from "../Admin/Admin";
import { TableDefiniton } from "./Types";
export interface FrontworkComponent<
Ifc extends RPCInterface = RPCInterface,
Name extends keyof Ifc = keyof Ifc,
SubresT = {}
> extends
PrivilegedRPCExporter<Ifc, Name, SubresT>,
TableDefinitionExporter
{
admin:FrontworkAdmin
name: string;
exportRPCFeatures(): RPCExporter<Ifc, Name, SubresT>[]
exportRPCs(): RPC<string, AnyFunction, {}>[]
getTableDefinitions(): TableDefiniton[]
initialize?(): Promise<any>
onSetAdmin?(admin:FrontworkAdmin): void
}
@@ -2,4 +2,4 @@ import { TableDefiniton } from "./Types";
export interface TableDefinitionExporter{
getTableDefinitions(): TableDefiniton[]
}
}
+64
View File
@@ -0,0 +1,64 @@
export const T1 = [
"Robe of Volatile Power",
"Salamander Scale Pants",
"Heavy Dark Iron Ring",
"Ring of Spell Power",
"Sorcerous Dagger",
"Wristguards of Stability",
"Helm of the Lifegiver",
"Fire Runed Grimoire",
"Crimson Shocker",
"Mana Igniting Cord",
"Quick Strike Ring",
"Seal of the Archmagus",
"Talisman of Ephemeral Power",
"Flameguard Gauntlets",
"Magma Tempered Boots",
"Obsidian Edged Blade",
"Aged Core Leather Gloves",
"Sabatons of the Flamewalker",
"Striker's Mark",
"Medallion of Steadfast Might",
"Earthshaker",
"Brutality Blade",
"Aurastone Hammer",
"Gutgore Ripper",
"Drillborer Disk",
"Azuresong Mageblade",
"Staff of Dominance",
"Blastershot Launcher",
"The Eye of Divinity",
"Ancient Petrified Leaf",
"Finkle's Lava Dredger",
"Core Hound Tooth",
"Core Forged Greaves",
"Gloves of the Hypnotic Flame",
"Sash of Whispered Secrets",
"Wild Growth Spaulders",
"Fireproof Cloak",
"Wristguards of True Flight",
"Fireguard Shoulders",
"Cauterizing Band",
"Onslaught Girdle",
"Perdition's Blade",
"Cloak of the Shrouded Mists",
"Band of Accuria",
"Crown of Destruction",
"Choker of the Fire Lord",
"Band of Sulfuras",
"Dragon's Blood Cape",
"Malistar's Defender",
"Spinal Reaper",
"Bonereaver's Edge",
"Shard of the Flame",
"Essence of the Pure Flame",
"Eye of Sulfuras",
"Deathbringer",
"Vis'kag the Bloodletter",
"Sapphiron Drape",
"Ancient Cornerstone Grimoire",
"Eskhandar's Collar",
"Ring of Binding",
"Shard of the Scale",
"Head of Onyxia",
"Bindings of the Windseeker"]
@@ -1,4 +1,4 @@
import { FrontworkAdmin } from "./Admin"
import { FrontworkAdmin } from "../Admin/Admin"
import { RPC, RPCExporter } from "rpclibrary"
import { TableDefiniton } from "./Types"
import { TableDefinitionExporter } from "./Interfaces"
@@ -0,0 +1,7 @@
import { RPCExporter, RPCInterface, RPC, RPCInterfaceArray } from "rpclibrary";
export interface PrivilegedRPCExporter <Ifc extends RPCInterface = RPCInterface, Name extends keyof Ifc = keyof Ifc, SubresT = {}>
extends RPCExporter<any,any>{
exportRPCFeatures() : RPCExporter<Ifc, Name, SubresT>[]
}
+91
View File
@@ -0,0 +1,91 @@
import * as Knex from "knex"
import { RPCExporter } from "rpclibrary";
export declare type NotificationSeverity = 'Info' | 'Important' | 'Error';
export type AdminConf = {
httpPort: number,
dbConf:Knex.Config,
eventBusConf: { [topic in string]: NotificationSeverity}
}
export type TableDefiniton = {
name: string,
tableBuilder: (table: Knex.CreateTableBuilder) => void
}
export type Rank = "ADMIN" | "Guildmaster" | "Officer" | "Classleader" | "Raider" | "Trial" | "Social" | "Guest"
export const _Rank : Rank[] = ["Guildmaster" , "Officer" , "Classleader" , "Raider" , "Trial" , "Social" , "Guest"]
export type Class = "Warrior" | "Rogue" | "Hunter" | "Mage" | "Warlock" | "Priest" | "Shaman" | "Paladin" | "Druid"
export const _Class : Class[] = ["Warrior" , "Rogue" , "Hunter" , "Mage" , "Warlock" , "Priest" , "Shaman" , "Paladin" , "Druid"]
export type AnyRPCExporter = RPCExporter<any,any>
export type RPCPermission = {
name: string
} & {
[rank in Rank] : boolean
}
export type User = {
id?: number
name: string
pwhash: string
class: Class
rank: Rank
email?: string
}
export type Raid = {
id?: number
title: string
description: string
start: string
minrank: Rank
}
export type Signup = {
raid_id: number
user_id: number
}
export type Token = {
value: string
user_id: number
created?: number
}
export type Auth = {port: number, user: User, token: Token}
export type FrontcraftFeatureIfc = RaidManagerFeatureIfc & LoginManagerFeatureIfc
export type LoginManagerIfc = {
Authenticator: {
login: (username:string, pwHash:string) => Promise<Token>
authenticate: (token:string | Token) => Promise<Auth>
}
}
export type LoginManagerFeatureIfc = {
createUser: {
createUser: (user:User) => Promise<User>
}
modifyPermissions: {
setPermission: (perm: RPCPermission) => Promise<void>
getPermissions: () => Promise<RPCPermission[]>
}
}
export type RaidManagerFeatureIfc = {
manageRaid: {
createRaid: (raid:Raid) => Promise<any>
addSignup: (signup: Signup) => Promise<any>
removeSignup: (signup: Signup) => Promise<any>
}
signup: {
getRaids: () => Promise<Raid[]>
getSingups: (raid:Raid) => Promise<Signup[]>
sign: (user:User, raid:Raid, attending:boolean) => Promise<any>
}
}