Files
muuhd/server/states/authState.js
Kim Ravn Hansen ba293d08b3 things+stuff
2025-09-10 12:36:42 +02:00

186 lines
5.8 KiB
JavaScript
Executable File

import * as msg from "../utils/messages.js";
import * as security from "../utils/security.js";
import { PlayerCreationState } from "./playerCreationState.js";
import { JustLoggedInState } from "./justLoggedIn.js";
import { Session } from "../models/session.js";
import { Config } from "../config.js";
const STATE_EXPECT_USERNAME = "promptUsername";
const STATE_EXPECT_PASSWORD = "promptPassword";
const USERNAME_PROMPT = [
"Please enter your _username_:",
"((type *:create* if you want to create a new user))",
];
const PASSWORD_PROMPT = "Please enter your password";
const ERROR_INSANE_PASSWORD = "Invalid password.";
const ERROR_INSANE_USERNAME =
"Username invalid, must be at 4-20 characters, and may only contain [a-z], [A-Z], [0-9] and underscore";
const ERROR_INCORRECT_PASSWOD = "Incorrect password.";
/** @property {Session} session */
export class AuthState {
subState = STATE_EXPECT_USERNAME;
/**
* @param {Session} session
*/
constructor(session) {
/** @type {Session} */
this.session = session;
}
onAttach() {
this.session.sendFigletMessage("M U U H D");
this.session.sendPrompt("username", USERNAME_PROMPT);
}
/** @param {msg.ClientMessage} message */
onMessage(message) {
if (this.subState === STATE_EXPECT_USERNAME) {
this.receiveUsername(message);
return;
}
if (this.subState === STATE_EXPECT_PASSWORD) {
this.receivePassword(message);
return;
}
console.error(
"Logic error, we received a message after we should have been logged in",
);
this.session.sendError("I received a message didn't know what to do with!");
}
/** @param {msg.ClientMessage} message */
receiveUsername(message) {
//
// handle invalid message types
if (!message.isUsernameResponse()) {
console.debug("what?!", message);
this.session.sendError("Incorrect message type!");
this.session.sendPrompt("username", USERNAME_PROMPT);
return;
}
//
// Handle the creation of new players
if (message.username === ":create") {
// TODO:
// Set gamestate = CreateNewPlayer
//
// Also check if player creation is allowed in config/env
this.session.setState(new PlayerCreationState(this.session));
return;
}
//
// do basic syntax checks on usernames
if (!security.isUsernameSane(message.username)) {
this.session.sendError(ERROR_INSANE_USERNAME);
this.session.sendPrompt("username", USERNAME_PROMPT);
return;
}
this.player = this.session.game.getPlayer(message.username);
//
// handle invalid username
if (!this.player) {
//
// This is a security risk. In the perfect world we would allow the player to enter both
// username and password before kicking them out, but since the player's username is not
// an email address, and we discourage from using “important” usernames, then we tell the
// player that they entered an invalid username right away.
//
// NOTE FOR ACCOUNT CREATION
// Do adult-word checks, so we dont have Fucky_McFuckFace
// https://www.npmjs.com/package/glin-profanity
this.session.sendError("Incorrect username, try again");
this.session.sendPrompt("username", USERNAME_PROMPT);
return;
}
//
// username was correct, proceed to next step
this.subState = STATE_EXPECT_PASSWORD;
this.session.sendSystemMessage("salt", this.player.salt);
this.session.sendPrompt("password", PASSWORD_PROMPT);
}
/** @param {msg.ClientMessage} message */
receivePassword(message) {
//
// handle invalid message types
if (!message.isPasswordResponse()) {
this.session.sendError("Incorrect message type!");
this.session.sendPrompt("password", PASSWORD_PROMPT);
return;
}
//
// Check of the password is sane. This is both bad from a security point
// of view, and technically not necessary as insane passwords couldn't
// reside in the player lists. However, let's save some CPU cycles on
// not hashing an insane password 1000+ times.
// This is technically bad practice, but since this is just a game,
// do it anyway.
if (!security.isPasswordSane(message.password)) {
this.session.sendError(ERROR_INSANE_PASSWORD);
this.session.sendPrompt("password", PASSWORD_PROMPT);
return;
}
//
// Block users who enter bad passwords too many times.
if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
((this.blockedUntil = new Date() + Config.maxFailedLogins),
this.session.sendCalamity(
"You have been locked out for too many failed password attempts, come back later",
));
this.session.close();
return;
}
//
// Handle blocked users.
// They don't even get to have their password verified.
if (this.player.blockedUntil > new Date()) {
this.session.sendCalamity(
"You have been locked out for too many failed password attempts, come back later",
);
this.session.close();
return;
}
//
// Verify the password against the hash we've stored.
if (!security.verifyPassword(message.password, this.player.passwordHash)) {
this.session.sendError("Incorrect password!");
this.session.sendPrompt("password", PASSWORD_PROMPT);
this.player.failedPasswordsSinceLastLogin++;
this.session.sendDebug(
`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`,
);
return;
}
this.player.lastSucessfulLoginAt = new Date();
this.player.failedPasswordsSinceLastLogin = 0;
this.session.player = this.player;
//
// Password correct, check if player is an admin
if (this.player.isAdmin) {
// set state AdminJustLoggedIn
}
//
// Password was correct, go to main game
this.session.setState(new JustLoggedInState(this.session));
}
}