You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UserManager.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import { RPCServer, Socket } from "rpclibrary";
  2. import { Inject, Injectable } from "../../Injector/ServiceDecorator";
  3. import { FrontworkAdmin } from "../../Admin/Admin";
  4. import { GuildManager } from "../Guild/GuildManager";
  5. import { ItemManager } from "../Item/ItemManager";
  6. import { RaidManager } from "../Raid/RaidManager";
  7. import { CharacterManager } from "../Character/CharacterManager";
  8. import { UserManagerFeatureIfc, UserManagerIfc } from "./RPCInterface";
  9. import { FrontworkComponent } from "../../Types/FrontworkComponent";
  10. import { Rank, User, Auth, _Rank, TableDefiniton, RPCPermission, FrontcraftFeatureIfc, AnyRPCExporter, Token, UserRecord } from "../../Types/Types";
  11. import { IAdmin } from "../../Admin/Interface";
  12. import { IUserManager } from "./Interface";
  13. import { getLogger, Logger } from "log4js";
  14. import { saltedHash } from "../../Util/hash";
  15. const uuid = require('uuid/v4')
  16. const salt = "6pIbc6yjSN"
  17. const ONE_WEEK = 604800000
  18. type Serverstate = {
  19. server: RPCServer,
  20. port : number,
  21. allowed: string[]
  22. };
  23. @Injectable(IUserManager)
  24. export class UserManager
  25. implements FrontworkComponent<UserManagerIfc, UserManagerFeatureIfc>, IUserManager{
  26. name = "UserManager" as "UserManager"
  27. @Inject(IAdmin)
  28. private admin: FrontworkAdmin
  29. @Inject(GuildManager)
  30. private guild : GuildManager
  31. @Inject(ItemManager)
  32. private item : ItemManager
  33. @Inject(RaidManager)
  34. private raid : RaidManager
  35. @Inject(CharacterManager)
  36. private character : CharacterManager
  37. exporters :any[] = []
  38. rankServers : {[rank in Rank] : Serverstate}
  39. userLogins : {[username in string] : UserRecord} = {}
  40. exportRPCs = () => [
  41. this.login,
  42. this.logout,
  43. this.getAuth,
  44. this.checkToken,
  45. this.createUser,
  46. this.getUser
  47. ]
  48. exportRPCFeatures = () => [{
  49. name: 'modifyPermissions' as 'modifyPermissions',
  50. exportRPCs: () => [
  51. this.getPermissions,
  52. this.setPermission
  53. ]
  54. },{
  55. name: 'softreserveCurrency' as 'softreserveCurrency',
  56. exportRPCs: () => [
  57. this.incrementCurrency,
  58. this.decrementCurrency,
  59. this.setCurrency
  60. ]
  61. }]
  62. changeRank = async (user:User, rank:Rank): Promise<User> => {
  63. await this.admin
  64. .knex('users')
  65. .where({
  66. user:user.username
  67. }).update({
  68. rank: rank
  69. })
  70. return await this.admin
  71. .knex('users')
  72. .where({
  73. user: user.username
  74. }).first()
  75. }
  76. getTableDefinitions = (): TableDefiniton[] => [
  77. {
  78. name: 'users',
  79. tableBuilder: (table) => {
  80. table.increments("id").primary()
  81. table.string("username").notNullable().unique()
  82. table.string("pwhash").notNullable()
  83. table.string("rank").notNullable()
  84. table.string("email").nullable().unique()
  85. table.integer("currency").defaultTo(1)
  86. }
  87. },{
  88. name: 'rpcpermissions',
  89. tableBuilder: (table) => {
  90. table.string("rpcname").primary().notNullable()
  91. _Rank.forEach(r => {
  92. if(r === 'ADMIN')
  93. table.boolean(r).defaultTo(true).notNullable()
  94. else
  95. table.boolean(r).defaultTo(false).notNullable()
  96. })
  97. }
  98. }
  99. ,...this.exporters.flatMap(exp => exp['getTableDefinitions']?exp['getTableDefinitions']():undefined)
  100. ]
  101. initialize = async () => {
  102. this.exporters = [this.guild, this.item, this.raid, this.character]
  103. //set up permissions
  104. getLogger('UserManager').debug('inserting permissions')
  105. await Promise.all(
  106. [this, ...this.exporters].flatMap(exp => exp.exportRPCFeatures().map(async (feature) => {
  107. try{
  108. await this.admin.knex.insert({ rpcname: feature.name }).into('rpcpermissions')
  109. }catch(e){
  110. getLogger('UserManager').debug(feature.name);
  111. }
  112. })))
  113. //start rankServers
  114. getLogger('UserManager').debug('Starting rank servers')
  115. let rankServers = { } as any
  116. await Promise.all(_Rank.map(async (r,i) => {
  117. const port = 20001 + i
  118. const rankServer = await this.startRankServer(r, port)
  119. rankServers[r] = {
  120. server: rankServer,
  121. port: port,
  122. allowed: []
  123. }
  124. }))
  125. this.rankServers = rankServers
  126. setInterval(this.checkExpiredSessions, 600_000)
  127. }
  128. stop = async () => {
  129. Object.values(this.userLogins).forEach(x => Object.values(x.connections).forEach(c => c.destroy()))
  130. await Promise.all(Object
  131. .values(this.rankServers)
  132. .map(async state => {
  133. try{
  134. //return await state.server.destroy()
  135. }catch(e){
  136. getLogger('UserManager').warn(e)
  137. }
  138. })
  139. );
  140. }
  141. checkExpiredSessions = () => {
  142. Object.values(this.userLogins).map(userLogin => {
  143. const auth = userLogin.auth
  144. if(!this.checkToken(auth.token.value, auth.user.rank)){
  145. this.logout(auth.user.username, auth.token.value)
  146. }
  147. })
  148. }
  149. checkConnection = async (socket: Socket) => {
  150. let data : any
  151. let tries = 0
  152. while(!data){
  153. tries ++
  154. if(tries === 5){
  155. getLogger('UserManager').debug('Connection check failed for connection *'+socket.port)
  156. socket.destroy()
  157. return false
  158. }
  159. data = await Promise.race([socket.call('getUserData'), new Promise((res, rej) => { setTimeout(res, 1000);})])
  160. }
  161. if(!this.userLogins[data.user.username]){
  162. await this.logout(data.user.username, data.token.value)
  163. return false
  164. }
  165. this.userLogins[data.user.username].connections[socket.port] = socket
  166. await socket.call('navigate', ["/frontcraft/dashboard"])
  167. return true
  168. }
  169. setPermission = async (permission: RPCPermission) => {
  170. await this.admin.knex('rpcpermissions')
  171. .where('rpcname', '=', permission.rpcname)
  172. .update(permission)
  173. }
  174. getPermissions = async () : Promise<RPCPermission[]> => {
  175. return await this.admin.knex.select('*').from('rpcpermissions')
  176. }
  177. getPermission = async (feature: keyof FrontcraftFeatureIfc, rank:Rank) : Promise<boolean> => {
  178. const perm : RPCPermission = await this.admin.knex
  179. .select(rank)
  180. .from('rpcpermissions')
  181. .where('rpcname', '=', <string>feature)
  182. .first()
  183. if(!perm) return false
  184. return perm[rank]
  185. }
  186. getRPCForRank = async (rank: Rank): Promise<AnyRPCExporter[]> => {
  187. let rpcs = [
  188. ...this.exportRPCFeatures(),
  189. ...this.exporters.flatMap((exp) => exp.exportRPCFeatures())
  190. ]
  191. const bits = await Promise.all(rpcs.map(async (feature) => {
  192. const allowed = await this.getPermission(<keyof FrontcraftFeatureIfc> feature.name, rank)
  193. return allowed
  194. }))
  195. return rpcs.filter(entry => bits.shift())
  196. }
  197. createUser = async(user:User): Promise<User> => {
  198. if(user.rank === 'ADMIN'){
  199. const admins = await this.admin.knex
  200. .select("*")
  201. .from('users')
  202. .where({rank: 'ADMIN'})
  203. if(admins.length > 0){
  204. return {} as User
  205. }
  206. }
  207. user.username = user.username.toLowerCase()
  208. user.pwhash = await saltedHash(user.pwhash, salt)
  209. await this.admin.knex('users')
  210. .insert(user)
  211. const userRecord = await this.admin.knex
  212. .select("*")
  213. .from('users')
  214. .where(user)
  215. .first()
  216. return userRecord
  217. }
  218. getUser = async (username: string) : Promise<User | void> => await this.admin
  219. .knex('users')
  220. .select('*')
  221. .where({
  222. username: username.toLowerCase()
  223. })
  224. .first()
  225. logout = async (username:string, tokenValue : string) : Promise<void> => {
  226. try{
  227. if(!this.checkTokenOwnedByUser(username, tokenValue)) return
  228. if(this.userLogins[username]){
  229. await Promise.all (Object.values(this.userLogins[username].connections).map(async (sock) => {
  230. await sock.call('navigate', '/auth/login')
  231. }))
  232. }
  233. Object.values(this.rankServers)
  234. .forEach(state => {
  235. state.allowed = state.allowed.filter(allowed => allowed !== tokenValue)
  236. })
  237. delete this.userLogins[username]
  238. }catch(e){
  239. console.log(e)
  240. }
  241. }
  242. wipeCurrency = async () => {
  243. await this.admin.knex('users')
  244. .update({currency: 0})
  245. }
  246. login = async(username:string, pwHash:string) : Promise<Auth> => {
  247. username = username.toLowerCase()
  248. const user:User = await this.admin.knex
  249. .select('*')
  250. .from('users')
  251. .where({ username: username })
  252. .first()
  253. const salted = await saltedHash(pwHash, salt)
  254. if(user && salted === user.pwhash){
  255. delete user.pwhash
  256. //return existing auth
  257. if(this.userLogins[username] != null){
  258. return this.userLogins[username].auth
  259. }
  260. const token = this.createToken(user)
  261. const userAuth : Auth = {
  262. token: token,
  263. user: user,
  264. port: this.rankServers[user.rank].port
  265. }
  266. this.userLogins[user.username] = {connections: {}, auth: userAuth, user:user}
  267. this.rankServers[user.rank].allowed.push(token.value)
  268. return userAuth
  269. }
  270. throw new Error('login failed')
  271. }
  272. getUserRecordByToken(tokenValue: string){
  273. return Object.values(this.userLogins).find(login => login.auth.token.value === tokenValue)
  274. }
  275. getAuth = async (tokenValue:string) : Promise<Auth | void> => {
  276. const maybeAuth = this.getUserRecordByToken(tokenValue)
  277. if(maybeAuth)
  278. return maybeAuth.auth
  279. return
  280. }
  281. startRankServer = async (rank : Rank, port: number) : Promise<RPCServer> => {
  282. const allowedRPCs = await this.getRPCForRank(rank)
  283. let rpcServer
  284. let n = 0
  285. while(!rpcServer){
  286. n++
  287. await Promise.race([
  288. new Promise((res, rej) => {
  289. rpcServer = new RPCServer(port, allowedRPCs, {
  290. closeHandler: (socket) => {
  291. Object.values(this.userLogins)
  292. .forEach(login => delete login.connections[socket.port])
  293. },
  294. connectionHandler: (socket) => {
  295. this.checkConnection(socket).then(res => {
  296. if(!res){
  297. socket.destroy();
  298. }
  299. }).catch((e) => {
  300. socket.destroy();
  301. getLogger('UserManager').warn(e);
  302. })
  303. },
  304. errorHandler: (socket, e, rpcName, args) => {
  305. getLogger('UserManager').error(rpcName, args, e);
  306. },
  307. sesame: (sesame) => this.checkToken(sesame, rank),
  308. visibility: '0.0.0.0'
  309. })
  310. res()
  311. }),
  312. new Promise((res, rej) => setTimeout(res, 500))
  313. ])
  314. if(!rpcServer && n>1)
  315. getLogger('UserManager').warn("createServer retry nr.", n, 'port', port)
  316. }
  317. return rpcServer
  318. }
  319. checkToken = (token: string, rank: Rank) : boolean => this.rankServers[rank].allowed.includes(token)
  320. && Object.values(this.userLogins).find(login => login.auth.token.value === token)!.auth.token.created > Date.now() - ONE_WEEK
  321. checkTokenOwnedByUser = (username: string, tokenValue: string) => {
  322. username = username.toLowerCase()
  323. const maybeRecord = this.getUserRecordByToken(tokenValue)
  324. if(!maybeRecord || maybeRecord.auth.user.username != username){
  325. getLogger('UserManager').warn(`Bad logout attempt
  326. token by: ${maybeRecord?maybeRecord.auth.user.username:tokenValue}
  327. tried to logout: ${username}`)
  328. return false
  329. }
  330. return true
  331. }
  332. createToken = (user:User): Token => {
  333. if(this.userLogins[user.username]){
  334. return this.userLogins[user.username].auth.token
  335. }
  336. const token:Token = {
  337. value: uuid(),
  338. user_id: user.id!,
  339. created: Date.now()
  340. }
  341. return token
  342. }
  343. getCurrency = async(user:User) : Promise<number> => {
  344. const usr : User = await this.admin
  345. .knex('users')
  346. .where('username', '=', user.username)
  347. .select('*')
  348. .first()
  349. return usr.currency!
  350. }
  351. decrementCurrency = async (user: User, value = 1) => {
  352. if(value < 1) return
  353. const usr : User = await this.admin
  354. .knex('users')
  355. .where('id', '=', user.id)
  356. .select('*')
  357. .first()
  358. if(!usr || usr.currency! <= 0) return
  359. await this.admin
  360. .knex('users')
  361. .where('id', '=', user.id)
  362. .decrement('currency', value)
  363. }
  364. incrementCurrency = async (user: User, value = 1) => {
  365. if(value < 1) return
  366. await this.admin
  367. .knex('users')
  368. .where('id', '=', user.id)
  369. .increment('currency', value)
  370. }
  371. setCurrency = async (user: User, value: number) => {
  372. if(value < 0) return
  373. await this.admin
  374. .knex('users')
  375. .where('id', '=', user.id)
  376. .update('currency', value)
  377. }
  378. }