UNPKG

reldens

Version:
548 lines (519 loc) 23.3 kB
/** * * Reldens - LoginManager * */ const { RoomGame } = require('../../rooms/server/game'); const { ActivePlayers } = require('./memory/active-players'); const { PasswordManager } = require('./password-manager'); const { RoomsConst } = require('../../rooms/constants'); const { GameConst } = require('../constants'); const { Logger, sc } = require('@reldens/utils'); class LoginManager { constructor(props) { this.config = props.config; this.configServer = props.configServer; this.usersManager = props.usersManager; this.roomsManager = props.roomsManager; this.passwordManager = PasswordManager; this.appServer = props.appServer; this.mailer = props.mailer; this.themeManager = props.themeManager; this.events = sc.get(props, 'events', false); this.listenEvents(); this.defaultStatePosition = { x: this.config.get('client/map/tileData/width', 32) * 2, y: this.config.get('client/map/tileData/height', 32) * 2, dir: GameConst.DOWN }; this.disconnectUsersOnServerChange = this.config.getWithoutLogs( 'server/players/disconnectUsersOnServerChange', true ); this.allowGuestUserName = this.config.getWithoutLogs('client/general/users/allowGuestUserName', false); this.guestEmailDomain = this.config.getWithoutLogs('server/players/guestsUser/emailDomain'); this.activePlayers = ActivePlayers; this.activePlayers.guestsEmailDomain = this.guestEmailDomain; this.serverSelfUrl = this.configServer.publicUrl || this.configServer.host +':'+this.configServer.port; this.roomsPerServer = this.mapRoomsServers(); } listenEvents() { if(!this.events){ Logger.error('EventsManager undefined in LoginManager.'); return false; } this.events.on('reldens.serverBeforeListen', async (props) => { await props.serverManager.app.post(GameConst.ROUTE_PATHS.DISCONNECT_USER, async (req, res) => { let disconnectedUserResult = {isSuccess: await this.disconnectUserByLoginData(req)}; //Logger.debug('Disconnected user result:', disconnectedUserResult); res.json(disconnectedUserResult); }); // @TODO - BETA - Refactor, move into the initial request data and avoid the extra requests from the client. await props.serverManager.app.get(GameConst.ROUTE_PATHS.MAILER, async (req, res) => { res.json({ enabled: this.mailer?.isEnabled() }); }); props.serverManager.app.get(GameConst.ROUTE_PATHS.TERMS_AND_CONDITIONS, (req, res) => { let languageParam = req.query.lang || ''; let termsConfig = this.config.getWithoutLogs( 'client/login/termsAndConditions/'+languageParam, this.config.getWithoutLogs( 'client/login/termsAndConditions', {} ) ); res.json({ link: sc.get(termsConfig, 'link', ''), heading: sc.get(termsConfig, 'heading', ''), body: sc.get(termsConfig, 'body', ''), checkboxLabel: sc.get(termsConfig, 'checkboxLabel', '') }); }); }); } async disconnectUserByLoginData(req) { let userData = req.body; if(!userData || !userData?.username){ //Logger.debug('Missing user data in request body.', req.body); return false; } //Logger.debug('Disconnect user by login data:', userData?.username); let activePlayer = this.activePlayers.fetchByRoomAndUserName( userData.username, this.activePlayers.gameRoomInstanceId ); if(!activePlayer){ //Logger.debug('Missing active player.'); return true; } if(!activePlayer.userModel){ //Logger.debug('Missing active player user model.'); return false; } if(!await this.login(activePlayer.userModel, userData)){ return false; } return await this.disconnectUserFromEveryRoom(activePlayer.userModel); } mapRoomsServers() { let roomsServersConfig = this.config.getWithoutLogs('client/rooms/servers', {}); let roomsServers = {}; for(let roomName of Object.keys(roomsServersConfig)){ if(!roomsServers[roomsServersConfig[roomName]]){ roomsServers[roomsServersConfig[roomName]] = []; } roomsServers[roomsServersConfig[roomName]].push(roomName); } // Logger.debug('Mapped rooms servers:', roomsServers); return roomsServers; } async broadcastDisconnectionMessage(userModel, options) { if(!this.disconnectUsersOnServerChange){ //Logger.debug('Configuration "disconnectUsersOnServerChange" is disabled.'); return true; } let roomServersList = Object.keys(this.roomsPerServer); if(0 === roomServersList.length){ //Logger.debug('None Rooms in servers list to trigger disconnection.'); return true; } for(let serverUrl of roomServersList){ if(this.serverSelfUrl === serverUrl){ //Logger.debug('Current host is the room serverUrl:', this.serverSelfUrl, serverUrl); continue; } //Logger.debug('Try disconnection from server URL:', serverUrl); let disconnectionResult = await this.disconnectFromServer(serverUrl, options); if(!disconnectionResult){ //Logger.debug('Disconnection result false.', disconnectionResult); return false; } } //Logger.debug('User disconnected from other servers.'); return true; } async disconnectFromServer(serverUrl, options) { let disconnectUrl = serverUrl+GameConst.ROUTE_PATHS.DISCONNECT_USER; let body = JSON.stringify(options); //Logger.debug('Disconnect from server "'+disconnectUrl+'", sending body: '+body); let result = false; try { let response = await fetch( disconnectUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, body, } ); let responseData = await response?.json(); //Logger.debug('Disconnect from server response status: '+response.status, 'Response data:', responseData); result = responseData.isSuccess; } catch (error) { Logger.error('Disconnect from server error.', serverUrl, error.message); // if secondary server is down we will allow the user to login on the other server result = true; } return result; } async disconnectUserFromEveryRoom(userModel, avoidGameRoom = false) { Logger.debug('Disconnect logged user: '+userModel.username); let createdRoomsKeys = Object.keys(this.roomsManager.createdInstances); for(let i of createdRoomsKeys){ let roomScene = this.roomsManager.createdInstances[i]; if(avoidGameRoom && roomScene instanceof RoomGame){ // there must be a single instance of RoomGame and we need to avoid it from disconnection: Logger.debug('Avoiding RoomGame disconnection.'); continue; } let activePlayer = roomScene.activePlayerByUserName(userModel.username, roomScene.roomId); if(!activePlayer){ Logger.debug( 'Active player not found by username "'+userModel.username+'" in room: ' +roomScene.roomName+' (ID: '+roomScene.roomId+').' ); continue; } if(!sc.isFunction(roomScene.disconnectBySessionId)){ Logger.warning('RoomScene type "'+typeof roomScene+'" does not have a "disconnectBySessionId" method: ' +roomScene.roomName+' (ID: '+roomScene.roomId+').'); continue; } await roomScene.disconnectBySessionId(activePlayer.sessionId, activePlayer.client, userModel); } return true; } async processUserRequest(userData = false) { if(sc.hasOwn(userData, 'forgot')){ return await this.processForgotPassword(userData); } this.events.emitSync('reldens.processUserRequestIsValidDataBefore', this, userData); let result = {error: 'Invalid user data.'}; if(!this.hasValidUserName(userData)){ Logger.debug('Missing username.', userData); this.events.emitSync('reldens.invalidData', this, userData, result); return result; } let user = await this.usersManager.loadUserByUsername(userData.username); if(user && userData.isGuest && userData.isNewUser){ Logger.debug('Guest login invalid user data.', userData); this.events.emitSync('reldens.guestLoginInvalidParams', this, user, userData, result); return result; } if(user){ return await this.login(user, userData); } if(userData.isGuest){ userData = this.overrideWithGuestData(userData); return await this.register(userData); } if(!userData.isNewUser){ Logger.info('Invalid user login data.', {user: userData.username}); this.events.emitSync('reldens.loginInvalidParams', this, user, userData, result); } if(userData.isNewUser){ user = await this.usersManager.loadUserByEmail(userData.email); if(user){ Logger.info('User already exists.', userData); this.events.emitSync('reldens.registrationInvalidParams', this, user, userData, result); } if(!user){ return await this.register(userData); } } return result; } overrideWithGuestData(userData) { if(-1 === userData.username.indexOf('guest-')){ let guestNameWithTimestamp = 'guest-' + sc.getTime() + '-'; userData.username = this.allowGuestUserName ? userData.username.replace('guest-', guestNameWithTimestamp) : guestNameWithTimestamp; } userData.email = userData.username+this.guestEmailDomain; userData.password = sc.randomChars(12); return userData; } hasValidUserName(userData) { return !(!userData || !sc.hasOwn(userData, 'username') || !userData.username.length); } async login(user, userData) { let result = {error: 'Login, invalid user data.'}; // check guest user: if(!this.isValidGuestLogin(userData, user)){ Logger.error('Guest user is not active for login.', userData); this.events.emitSync('reldens.loginInvalidRole', this, user, userData, result); return result; } // check if the passwords match: if(!this.passwordManager.validatePassword(userData.password, user.password)){ Logger.error('Invalid password for user login.', userData); this.events.emitSync('reldens.loginInvalidPassword', this, user, userData, result); return result; } try { if(sc.isArray(user.players) && 0 < user.players.length){ // set the scene on the user players: this.events.emitSync('reldens.setSceneOnPlayers', this, user, userData); await this.setSceneOnPlayers(user, userData); } let result = {user: user}; this.events.emitSync('reldens.loginSuccess', this, user, userData, result); return result; } catch (error) { Logger.error('Login try/catch error.', error, userData); this.events.emitSync('reldens.loginError', this, user, userData, result); return result; } } isValidGuestLogin(userData, user) { let guestRoleId = this.config.server?.players?.guestUser?.roleId || 0; if(0 === guestRoleId){ Logger.warning('Guest role ID is not defined by configuration.'); return true; } if(!userData.isGuest && user.role_id !== guestRoleId){ return true; } return Boolean(this.activePlayers.fetchByRoomAndUserName(user.username, this.activePlayers.gameRoomInstanceId)); } async setSceneOnPlayers(user, userData) { for(let player of user.players){ if(!player.state){ continue; } let config = this.config.get('client/rooms/selection'); if( config.allowOnLogin && userData['selectedScene'] && userData['selectedScene'] !== RoomsConst.ROOM_LAST_LOCATION_KEY ){ await this.applySelectedLocation(player, userData['selectedScene']); } player.state.scene = await this.getRoomNameById(player.state.room_id); } } async roleAuthenticationCallback(email, password, roleId = 0) { let user = await this.usersManager.loadUserByEmail(email); let validatedRole = 0 === roleId || String(user.role_id) === String(roleId); if(user && validatedRole){ let result = this.passwordManager.validatePassword( password, user.password ); if(result){ return user; } } return false; } async applySelectedLocation(player, selectedScene) { let selectedRoom = await this.roomsManager.loadRoomByName(selectedScene); if(!selectedRoom){ return false; } player.state = this.getStateObjectFromRoom(selectedRoom); } async getRoomNameById(roomId) { let playerRoom = await this.roomsManager.loadRoomById(roomId); if(playerRoom){ return playerRoom.roomName; } return GameConst.ROOM_NAME_MAP; } async register(userData) { let result = {error: 'Registration error, there was an error with your request, please try again later.'}; if(!userData.isNewUser){ Logger.error('Registration invalid parameters.', userData); await this.events.emit('reldens.register', this, userData, result); return result; } try { // if the email doesn't exist in the database, and it's a registration request: // insert user, player, player state, player stats, class path: let defaultRoleId = this.config.server.players.initialUser.roleId; let roleId = !userData.isGuest ? defaultRoleId : this.config.server.players.guestUser.roleId; let newUser = await this.usersManager.createUser({ email: userData.email, username: userData.username, password: this.passwordManager.encryptPassword(userData.password), role_id: roleId, status: this.config.server.players.initialUser.status }); let result = {user: newUser}; await this.events.emit('reldens.createNewUserAfter', newUser, this, result); return result; } catch (error) { Logger.error('Registration try/catch error.', error, userData); await this.events.emit('reldens.createNewUserError', this, userData, result); return result; } } async createNewPlayer(loginData) { // @TODO - BETA - Replace all result.message hardcoded values by snippets. let minimumPlayerNameLength = this.config.getWithoutLogs('client/players/name/minimumLength', 3); if(minimumPlayerNameLength > loginData['new-player-name'].toString().length){ let result = {error: true, message: 'Invalid player name, please choose another name.'}; await this.events.emit('reldens.playerNewName', this, loginData, result); return result; } let initialState = await this.prepareInitialState(loginData['selectedScene']); if(!await this.validateInitialState(initialState)){ let result = { error: true, message: 'There was an error with the player initial state, please contact the administrator.' }; await this.events.emit('reldens.playerSceneUnavailable', this, loginData, result); return result; } let playerData = { name: loginData['new-player-name'], user_id: loginData.user_id, state: initialState }; await this.events.emit('reldens.createNewPlayerBefore', loginData, playerData, this); let isNameAvailable = await this.usersManager.isNameAvailable(playerData.name); if(!isNameAvailable){ let result = {error: true, message: 'The player name is not available, please choose another name.'}; await this.events.emit('reldens.playerNewNameUnavailable', this, loginData, isNameAvailable, result); return result; } try { let player = await this.usersManager.createPlayer(playerData); player.state.scene = await this.getRoomNameById(initialState.room_id); let result = {error: false, player}; await this.events.emit('reldens.createdNewPlayer', player, loginData, this, result); return result; } catch (error) { Logger.error('Player creation error.', error.message); let result = {error: true, message: 'There was an error creating your player, please try again.'}; await this.events.emit('reldens.createNewPlayerCriticalError', this, loginData, error, result); return result; } } async validateInitialState(initialState) { if(!initialState){ return false; } let roomId = sc.get(initialState, 'room_id', false); if(false === roomId){ return false; } return await this.roomsManager.loadRoomById(roomId); } async prepareInitialState(roomName) { let config = this.config.get('client/rooms/selection'); let initialState = this.config.server.players.initialState; if(!config.allowOnRegistration || !roomName){ if(!initialState){ Logger.critical('Initial state is not defined!'); return false; } return initialState; } let selectedRoom = await this.roomsManager.loadRoomByName(roomName); if(!selectedRoom){ if(!initialState){ Logger.critical('Initial state is not defined!'); return false; } return initialState; } return this.getStateObjectFromRoom(selectedRoom); } getStateObjectFromRoom(selectedRoom) { // clone default position and set the room id: let stateData = Object.assign({room_id: selectedRoom.roomId}, this.defaultStatePosition); if(selectedRoom.returnPointDefault){ stateData.x = selectedRoom.returnPointDefault[RoomsConst.RETURN_POINT_KEYS.X]; stateData.y = selectedRoom.returnPointDefault[RoomsConst.RETURN_POINT_KEYS.Y]; stateData.dir = selectedRoom.returnPointDefault[RoomsConst.RETURN_POINT_KEYS.DIRECTION]; } return stateData; } async updateLastLogin(userModel) { let updated = await this.usersManager.updateUserLastLogin(userModel); if(!updated){ // @TODO - BETA - Logout user. Logger.error('Last login update fail on user with ID "'+userModel.id+'".'); } return updated; } async processForgotPassword(userData) { // @TODO - WIP - TRANSLATIONS. if(!this.mailer.isEnabled()){ return {error: 'The forgot password email can not be send, please contact the administrator.'}; } if(!sc.hasOwn(userData, 'email')){ return {error: 'Please complete your email.'}; } let existsMessage = {error: 'If the email exists then a reset password link should be received soon.'}; let user = await this.usersManager.loadUserByEmail(userData.email); if(!user){ return existsMessage; } let forgotLimit = Number(process.env.RELDENS_MAILER_FORGOT_PASSWORD_LIMIT) || 4; let microLimit = Date.now() - (forgotLimit * 60 * 60 * 1000); let statusInt = Number(user.status); if(statusInt >= microLimit){ Logger.debug('Reset link already sent to "'+userData.email+'".', statusInt); return existsMessage; } let sendResult = {result: await this.sendForgotPasswordEmail(userData, user.password)}; if(sendResult.result){ Logger.debug('Reset link sent to "'+userData.email+'".'); await this.usersManager.updateUserByEmail(userData.email, {status: Date.now()}); } this.events.emitSync('reldens.processForgotPassword', this, userData, sendResult); return existsMessage; } async sendForgotPasswordEmail(userData, oldPassword) { // @TODO - WIP - TRANSLATIONS. let emailPath = this.themeManager.assetPath('email', 'forgot.html'); let serverUrl = this.config.server.publicUrl || this.config.server.baseUrl; let resetLink = serverUrl+'/reset-password?email='+userData.email+'&id='+oldPassword; let subject = this.config.getWithoutLogs('server/mailer/forgotPassword/subject', 'Forgot password'); let content = await this.themeManager.loadAndRenderTemplate(emailPath, {resetLink: resetLink}); // @TODO - BETA - Make all system messages configurable. try { return await this.mailer.sendEmail({ from: this.mailer.from, to: userData.email, subject, html: content }); } catch (error) { return false; } } } module.exports.LoginManager = LoginManager;