import { RPCServer, Socket } from "rpclibrary"; import { Inject, Injectable } from "../../Injector/ServiceDecorator"; import { FrontworkAdmin } from "../../Admin/Admin"; import { GuildManager } from "../Guild/GuildManager"; import { ItemManager } from "../Item/ItemManager"; import { RaidManager } from "../Raid/RaidManager"; import { CharacterManager } from "../Character/CharacterManager"; import { UserManagerFeatureIfc, UserManagerIfc } from "./RPCInterface"; import { FrontworkComponent } from "../../Types/FrontworkComponent"; import { Rank, User, Auth, _Rank, TableDefiniton, RPCPermission, FrontcraftFeatureIfc, AnyRPCExporter, Token, UserRecord } from "../../Types/Types"; import { IAdmin } from "../../Admin/Interface"; import { IUserManager } from "./Interface"; import { getLogger, Logger } from "log4js"; import { saltedHash } from "../../Util/hash"; const uuid = require('uuid/v4') const salt = "6pIbc6yjSN" const ONE_WEEK = 604800000 type Serverstate = { server: RPCServer, port : number, allowed: string[] }; @Injectable(IUserManager) export class UserManager implements FrontworkComponent, IUserManager{ name = "UserManager" as "UserManager" @Inject(IAdmin) private admin: FrontworkAdmin @Inject(GuildManager) private guild : GuildManager @Inject(ItemManager) private item : ItemManager @Inject(RaidManager) private raid : RaidManager @Inject(CharacterManager) private character : CharacterManager exporters :any[] = [] rankServers : {[rank in Rank] : Serverstate} userLogins : {[username in string] : UserRecord} = {} exportRPCs = () => [ this.login, this.logout, this.getAuth, this.checkToken, this.createUser, this.getUser ] exportRPCFeatures = () => [{ name: 'modifyPermissions' as 'modifyPermissions', exportRPCs: () => [ this.getPermissions, this.setPermission ] },{ name: 'softreserveCurrency' as 'softreserveCurrency', exportRPCs: () => [ this.incrementCurrency, this.decrementCurrency, this.setCurrency ] }] changeRank = async (user:User, rank:Rank): Promise => { await this.admin .knex('users') .where({ user:user.username }).update({ rank: rank }) return await this.admin .knex('users') .where({ user: user.username }).first() } getTableDefinitions = (): TableDefiniton[] => [ { name: 'users', tableBuilder: (table) => { table.increments("id").primary() table.string("username").notNullable().unique() table.string("pwhash").notNullable() table.string("rank").notNullable() table.string("email").nullable().unique() table.integer("currency").defaultTo(1) } },{ name: 'rpcpermissions', tableBuilder: (table) => { table.string("rpcname").primary().notNullable() _Rank.forEach(r => { if(r === 'ADMIN') table.boolean(r).defaultTo(true).notNullable() else table.boolean(r).defaultTo(false).notNullable() }) } } ,...this.exporters.flatMap(exp => exp['getTableDefinitions']?exp['getTableDefinitions']():undefined) ] initialize = async () => { this.exporters = [this.guild, this.item, this.raid, this.character] //set up permissions getLogger('UserManager').debug('inserting permissions') await Promise.all( [this, ...this.exporters].flatMap(exp => exp.exportRPCFeatures().map(async (feature) => { try{ await this.admin.knex.insert({ rpcname: feature.name }).into('rpcpermissions') }catch(e){ getLogger('UserManager').debug(feature.name); } }))) //start rankServers getLogger('UserManager').debug('Starting rank servers') let rankServers = { } as any await Promise.all(_Rank.map(async (r,i) => { const port = 20001 + i const rankServer = await this.startRankServer(r, port) rankServers[r] = { server: rankServer, port: port, allowed: [] } })) this.rankServers = rankServers setInterval(this.checkExpiredSessions, 600_000) } stop = async () => { Object.values(this.userLogins).forEach(x => Object.values(x.connections).forEach(c => c.destroy())) await Promise.all(Object .values(this.rankServers) .map(async state => { try{ //return await state.server.destroy() }catch(e){ getLogger('UserManager').warn(e) } }) ); } checkExpiredSessions = () => { Object.values(this.userLogins).map(userLogin => { const auth = userLogin.auth if(!this.checkToken(auth.token.value, auth.user.rank)){ this.logout(auth.user.username, auth.token.value) } }) } checkConnection = async (socket: Socket) => { let data : any let tries = 0 while(!data){ tries ++ if(tries === 5){ getLogger('UserManager').debug('Connection check failed for connection *'+socket.port) socket.destroy() return false } data = await Promise.race([socket.call('getUserData'), new Promise((res, rej) => { setTimeout(res, 1000);})]) } if(!this.userLogins[data.user.username]){ await this.logout(data.user.username, data.token.value) return false } this.userLogins[data.user.username].connections[socket.port] = socket await socket.call('navigate', ["/frontcraft/dashboard"]) return true } setPermission = async (permission: RPCPermission) => { await this.admin.knex('rpcpermissions') .where('rpcname', '=', permission.rpcname) .update(permission) } getPermissions = async () : Promise => { return await this.admin.knex.select('*').from('rpcpermissions') } getPermission = async (feature: keyof FrontcraftFeatureIfc, rank:Rank) : Promise => { const perm : RPCPermission = await this.admin.knex .select(rank) .from('rpcpermissions') .where('rpcname', '=', feature) .first() if(!perm) return false return perm[rank] } getRPCForRank = async (rank: Rank): Promise => { let rpcs = [ ...this.exportRPCFeatures(), ...this.exporters.flatMap((exp) => exp.exportRPCFeatures()) ] const bits = await Promise.all(rpcs.map(async (feature) => { const allowed = await this.getPermission( feature.name, rank) return allowed })) return rpcs.filter(entry => bits.shift()) } createUser = async(user:User): Promise => { if(user.rank === 'ADMIN'){ const admins = await this.admin.knex .select("*") .from('users') .where({rank: 'ADMIN'}) if(admins.length > 0){ return {} as User } } user.username = user.username.toLowerCase() user.pwhash = await saltedHash(user.pwhash, salt) await this.admin.knex('users') .insert(user) const userRecord = await this.admin.knex .select("*") .from('users') .where(user) .first() return userRecord } getUser = async (username: string) : Promise => await this.admin .knex('users') .select('*') .where({ username: username.toLowerCase() }) .first() logout = async (username:string, tokenValue : string) : Promise => { try{ if(!this.checkTokenOwnedByUser(username, tokenValue)) return if(this.userLogins[username]){ await Promise.all (Object.values(this.userLogins[username].connections).map(async (sock) => { await sock.call('navigate', '/auth/login') })) } Object.values(this.rankServers) .forEach(state => { state.allowed = state.allowed.filter(allowed => allowed !== tokenValue) }) delete this.userLogins[username] }catch(e){ console.log(e) } } wipeCurrency = async () => { await this.admin.knex('users') .update({currency: 0}) } login = async(username:string, pwHash:string) : Promise => { username = username.toLowerCase() const user:User = await this.admin.knex .select('*') .from('users') .where({ username: username }) .first() const salted = await saltedHash(pwHash, salt) if(user && salted === user.pwhash){ delete user.pwhash //return existing auth if(this.userLogins[username] != null){ return this.userLogins[username].auth } const token = this.createToken(user) const userAuth : Auth = { token: token, user: user, port: this.rankServers[user.rank].port } this.userLogins[user.username] = {connections: {}, auth: userAuth, user:user} this.rankServers[user.rank].allowed.push(token.value) return userAuth } throw new Error('login failed') } getUserRecordByToken(tokenValue: string){ return Object.values(this.userLogins).find(login => login.auth.token.value === tokenValue) } getAuth = async (tokenValue:string) : Promise => { const maybeAuth = this.getUserRecordByToken(tokenValue) if(maybeAuth) return maybeAuth.auth return } startRankServer = async (rank : Rank, port: number) : Promise => { const allowedRPCs = await this.getRPCForRank(rank) let rpcServer let n = 0 while(!rpcServer){ n++ await Promise.race([ new Promise((res, rej) => { rpcServer = new RPCServer(port, allowedRPCs, { closeHandler: (socket) => { Object.values(this.userLogins) .forEach(login => delete login.connections[socket.port]) }, connectionHandler: (socket) => { this.checkConnection(socket).then(res => { if(!res){ socket.destroy(); } }).catch((e) => { socket.destroy(); getLogger('UserManager').warn(e); }) }, errorHandler: (socket, e, rpcName, args) => { getLogger('UserManager').error(rpcName, args, e); }, sesame: (sesame) => this.checkToken(sesame, rank), visibility: '0.0.0.0' }) res() }), new Promise((res, rej) => setTimeout(res, 500)) ]) if(!rpcServer && n>1) getLogger('UserManager').warn("createServer retry nr.", n, 'port', port) } return rpcServer } checkToken = (token: string, rank: Rank) : boolean => this.rankServers[rank].allowed.includes(token) && Object.values(this.userLogins).find(login => login.auth.token.value === token)!.auth.token.created > Date.now() - ONE_WEEK checkTokenOwnedByUser = (username: string, tokenValue: string) => { username = username.toLowerCase() const maybeRecord = this.getUserRecordByToken(tokenValue) if(!maybeRecord || maybeRecord.auth.user.username != username){ getLogger('UserManager').warn(`Bad logout attempt token by: ${maybeRecord?maybeRecord.auth.user.username:tokenValue} tried to logout: ${username}`) return false } return true } createToken = (user:User): Token => { if(this.userLogins[user.username]){ return this.userLogins[user.username].auth.token } const token:Token = { value: uuid(), user_id: user.id!, created: Date.now() } return token } getCurrency = async(user:User) : Promise => { const usr : User = await this.admin .knex('users') .where('username', '=', user.username) .select('*') .first() return usr.currency! } decrementCurrency = async (user: User, value = 1) => { if(value < 1) return const usr : User = await this.admin .knex('users') .where('id', '=', user.id) .select('*') .first() if(!usr || usr.currency! <= 0) return await this.admin .knex('users') .where('id', '=', user.id) .decrement('currency', value) } incrementCurrency = async (user: User, value = 1) => { if(value < 1) return await this.admin .knex('users') .where('id', '=', user.id) .increment('currency', value) } setCurrency = async (user: User, value: number) => { if(value < 0) return await this.admin .knex('users') .where('id', '=', user.id) .update('currency', value) } }