| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- 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<UserManagerIfc, UserManagerFeatureIfc>, 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<User> => {
- 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<RPCPermission[]> => {
- return await this.admin.knex.select('*').from('rpcpermissions')
- }
-
- getPermission = async (feature: keyof FrontcraftFeatureIfc, rank:Rank) : Promise<boolean> => {
- const perm : RPCPermission = await this.admin.knex
- .select(rank)
- .from('rpcpermissions')
- .where('rpcname', '=', <string>feature)
- .first()
-
- if(!perm) return false
- return perm[rank]
- }
-
- getRPCForRank = async (rank: Rank): Promise<AnyRPCExporter[]> => {
- let rpcs = [
- ...this.exportRPCFeatures(),
- ...this.exporters.flatMap((exp) => exp.exportRPCFeatures())
- ]
-
- const bits = await Promise.all(rpcs.map(async (feature) => {
- const allowed = await this.getPermission(<keyof FrontcraftFeatureIfc> feature.name, rank)
- return allowed
- }))
-
- return rpcs.filter(entry => bits.shift())
- }
-
- createUser = async(user:User): Promise<User> => {
- 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<User | void> => await this.admin
- .knex('users')
- .select('*')
- .where({
- username: username.toLowerCase()
- })
- .first()
-
- logout = async (username:string, tokenValue : string) : Promise<void> => {
- 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<Auth> => {
- 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<Auth | void> => {
- const maybeAuth = this.getUserRecordByToken(tokenValue)
- if(maybeAuth)
- return maybeAuth.auth
- return
- }
-
- startRankServer = async (rank : Rank, port: number) : Promise<RPCServer> => {
-
- 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<number> => {
- 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)
- }
-
-
- }
|