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
@@ -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 []
}
}
+100
View File
@@ -0,0 +1,100 @@
import { RPCExporter, SubscriptionResponse, ErrorResponse, SuccessResponse } from "rpclibrary";
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'
export type Notification = {
ID?:number,
severity: NotificationSeverity,
topic: string,
message:string,
time?: number
}
export type EventbusIfc = {
Eventbus: {
getNotificationLog: () => Promise<Notification[]>
pushNotification: (notification:Notification) => Promise<void>
subscribeNotifications: (callback:Function) => Promise<SubscriptionResponse | ErrorResponse>
}
}
const logger = getLogger("Eventbus", 'debug')
export class FrontworkEventBus
implements RPCExporter<EventbusIfc, "Eventbus">, TableDefinitionExporter {
name = "Eventbus" as "Eventbus"
private subscriptions : { [uid in string]:Function } = {}
constructor(private admin: FrontworkAdmin){
}
async subscribeNotifications(callback) : Promise<SubscriptionResponse>{
const uid = uuid()
this.subscriptions[uid] = callback
return { result: 'Success', uuid: uid }
}
async getNotificationLog() : Promise<Notification[]>{
try{
return await this.admin.knex.select('*').from('notifications')
}catch(e){
logger.error(e)
throw e
}
}
async unsubscribeNotifications(uid:string) : Promise<SuccessResponse | ErrorResponse>{
if(!this.subscriptions[uid]) return { result: 'Error', message: "Unknown subscription" }
delete this.subscriptions[uid]
return { result: 'Success' }
}
async pushNotification(notification: Notification){
if(!notification.time) notification.time = Date.now()
logger.debug("inserting into notifications", notification)
try{
await this.admin.knex('notifications').insert(notification)
}catch(e){
logger.error(e)
throw e
}
Object.values(this.subscriptions).forEach(callback => {
callback(notification)
})
}
exportRPCs(){
return [{
name: 'getNotificationLog' as 'getNotificationLog',
call: async () => await this.getNotificationLog()
},{
name: 'pushNotification' as 'pushNotification',
call: async (notification: Notification) => { return await this.pushNotification(notification) }
},{
name: 'subscribeNotificaitons' as 'subscribeNotifications',
hook: async (callback: Function) => {
return await this.subscribeNotifications(callback)
}
}]
}
getTableDefinitions(): TableDefiniton[]{
return [{
name: 'notifications',
tableBuilder: (table) => {
table.increments('ID').primary();
table.string('severity');
table.string('topic');
table.string('message');
table.timestamp('time');
}
}]
}
}
+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]
}
}
}
+167
View File
@@ -0,0 +1,167 @@
import { RPCExporter } from "rpclibrary";
import { Git } from "upgiter"
import { FolderStatus } from "upgiter/js/src/Types";
import { Plugin } from "../Types/Plugin";
import { FrontworkAdmin } from "../Admin/Admin";
class PluginLoader {
private runningPlugins: Plugin[] = []
private pluginUpdaters:{[name in string]:Git.Updater} = {}
constructor(private admin:FrontworkAdmin){}
async selfUpdate(force:boolean = false){
const updater = new Git.Updater({
schema: 'https',
localPath: './dist',
remoteHost: 'www.versioncontrol.me',
remotePath: 'frontwork-distribution',
repoName: 'admin'
})
let status = await updater.getStatus()
if(force || !status.remote || !status.remote.includes("fb-dist/admin") || !status.exists || status.empty || !status.currentTag){
//logger.warn("Cloning fb-dist/admin into ./dist ..."+(force?" USING FORCE!":""))
status = await updater.cloneRepo(force)
const installer = eval("require")("./Installer").installAdmin
installer(this.getPlugins())
}
return status
}
async installPlugin(name: string, force:boolean = false):Promise<FolderStatus>{
//logger.warn("Cloning fb-dist/"+name+".git into ./plugins/"+name+" ..."+(force?" USING FORCE!":""))
this.pluginUpdaters[name] = new Git.Updater({
schema: 'https',
localPath: './plugins/'+name,
remoteHost: 'www.versioncontrol.me',
remotePath: 'frontblock-distribution',
repoName: name
})
const status = await this.pluginUpdaters[name].cloneRepo(force)
return status
}
async startPlugin(name:string):Promise<boolean>{
if(!this.pluginUpdaters[name]) return false
const status = await this.pluginUpdaters[name].getStatus()
if(!status.exists || status.empty || !status.tags || status.tags.length === 0){
if(status.currentTag && !status.latestTag){
//git glitches sometimes if you check immediately after clone
//logger.warn("re-fetching tag for "+name+"...")
return await this.startPlugin(name)
}
//logger.error("Bad repo status", name, status)
return false
}
/* if(this.loadedPlugins[name]){
logger.error("Plugin", name, "is already started")
return false
}
*/
let evalstr = "../plugins/"+name+"/Plugin"
const pluginClass = await eval('require')(evalstr)
const pluginObj = new pluginClass.default(this.admin)
try{
if(pluginObj.start)
await pluginObj.start()
//this.pushNotification({message: "Started plugin "+pluginObj.name, severity: "Important", topic:"admin"})
this.addPlugin(pluginObj)
return true
}catch(e){
//logger.error(e)
//this.pushNotification({message: "Start of plugin "+pluginObj.name+" failed because of "+String(e), severity: "Error", topic:"admin"})
return false
}
}
async updatePlugin(name:string):Promise<boolean>{
if(!this.pluginUpdaters[name]) return false
const status = await this.pluginUpdaters[name].getStatus()
if(!status.exists || status.empty || !status.tags || status.tags.length === 0){
//logger.error("Bad repo status", name, status)
return false
}
if(status.currentTag == status.latestTag){
//logger.warn(name, "already at latest tag")
return false
}
/*
if(this.loadedPlugins[name]){
logger.error("Plugin", name, "is running. Stop it first")
return false
}
*/
this.pluginUpdaters[name].checkoutTag(status.latestTag!)
return true
}
async setPluginVersion(pluginName:string, tag:string):Promise<FolderStatus>{
const status = await this.pluginUpdaters[pluginName].getStatus()
if(!status.exists || !status.tags || status.tags.length === 0 || !status.tags.includes(tag)){
//logger.error("Bad repo status", pluginName, status)
return status
}
return await this.pluginUpdaters[pluginName].checkoutTag(tag)
}
async addPlugin(plugin: Plugin){
this.runningPlugins.push(plugin)
if(plugin.getTableDefinitions().length != 0)
await this.admin.makeKnex()
}
removePlugin(plugin: Plugin){
this.runningPlugins = this.runningPlugins.filter(p => p.name !== plugin.name)
}
getPlugins():Plugin[]{
return [...this.runningPlugins]
}
}
export type PluginLoaderIfc = {
PluginLoader: {
installPlugin: (name:string, force:boolean) => Promise<FolderStatus>
startPlugin: (name:string) => Promise<boolean>
updatePlugin: (name:string) => Promise<boolean>
setPluginVersion: (name:string, tag:string) => Promise<FolderStatus>
getLoadedPluginNames: () => Promise<String[]>
selfUpdate: (force:boolean) => Promise<FolderStatus>
}
}
export class RPCPluginLoader
extends PluginLoader
implements RPCExporter<PluginLoaderIfc, "PluginLoader">{
name = "PluginLoader" as "PluginLoader";
exportRPCs(){
return [{
name: "installPlugin" as "installPlugin",
call: async (name:string, force = false) => {return await this.installPlugin(name, force)},
},{
name: "startPlugin" as "startPlugin",
call: async (name:string) => {return await this.startPlugin(name)},
},{
name: "updatePlugin" as "updatePlugin",
call: async (name:string) => {return await this.updatePlugin(name)},
},{
name: "setPluginVersion" as "setPluginVersion",
call: async (name:string, tag:string) => {return await this.setPluginVersion(name, tag)},
},{
name: "getLoadedPluginNames" as "getLoadedPluginNames",
call: async () => {return this.getPlugins().map(p => p.name)},
},{
name: "selfUpdate" as "selfUpdate",
call: async (force: boolean) => {return await this.selfUpdate(force)},
}]
}
}
+42
View File
@@ -0,0 +1,42 @@
import { ConfigLoader } from 'loadson'
import { RPCExporter } from 'rpclibrary'
export type ConfigLoaderIfc<ConfT> = {
Config : {
getConfig: () => ConfT
resetConfig: () => ConfT
setConfig: (conf:ConfT) => ConfT
setConfigKey: (key:string, value:any) => ConfT
deleteConfigKey: (key:string) => ConfT
getConfigKey: (key:string) => any
}
}
export class RPCConfigLoader<ConfT>
extends ConfigLoader<ConfT>
implements RPCExporter<ConfigLoaderIfc<ConfT>, "Config">{
name = "Config" as "Config"
exportRPCs() {
return [{
name: "getConfig" as "getConfig",
call: () => { return this.getConfig() }
},{
name: "resetConfig" as "resetConfig",
call: () => { return this.resetConfig() }
},{
name: "setConfig" as "setConfig",
call: (conf:ConfT) => { return this.setConfig(conf) }
},{
name: "setConfigKey" as "setConfigKey",
call: (key:string, value:any) => { return this.setConfigKey(key, value) }
},{
name: "deleteConfigKey" as "deleteConfigKey",
call: (key:string) => { return this.deleteConfigKey(key) }
},{
name: "getConfigKey" as "getConfigKey",
call: (key:string) => { return this.getConfigKey(key) }
}]
}
}
@@ -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!
})
}