Get login working correctly.

This commit is contained in:
Condorra 2022-12-27 16:08:27 +11:00
parent 16bd49f160
commit 672eb1ee75
10 changed files with 180 additions and 32 deletions

61
Cargo.lock generated
View File

@ -8,6 +8,15 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "aliasable" name = "aliasable"
version = "0.1.3" version = "0.1.3"
@ -119,6 +128,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
"uuid", "uuid",
"validator",
] ]
[[package]] [[package]]
@ -692,6 +702,17 @@ dependencies = [
"cxx-build", "cxx-build",
] ]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -785,6 +806,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.5" version = "0.10.5"
@ -1191,6 +1218,23 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -1792,7 +1836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna 0.3.0",
"percent-encoding", "percent-encoding",
] ]
@ -1812,6 +1856,21 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "validator"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
dependencies = [
"idna 0.2.3",
"lazy_static",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
]
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@ -31,3 +31,4 @@ nom = "7.1.1"
ouroboros = "0.15.5" ouroboros = "0.15.5"
chrono = { version = "0.4.23", features = ["serde"] } chrono = { version = "0.4.23", features = ["serde"] }
bcrypt = "0.13.0" bcrypt = "0.13.0"
validator = "0.16.0"

View File

@ -9,6 +9,7 @@ use tokio_postgres::NoTls;
use crate::message_handler::ListenerSession; use crate::message_handler::ListenerSession;
use crate::DResult; use crate::DResult;
use crate::models::{session::Session, user::User, item::Item}; use crate::models::{session::Session, user::User, item::Item};
use tokio_postgres::types::ToSql;
use serde_json; use serde_json;
use futures::FutureExt; use futures::FutureExt;
@ -226,6 +227,21 @@ impl DBTrans {
&details.username.to_lowercase()]).await?; &details.username.to_lowercase()]).await?;
Ok(()) Ok(())
} }
pub async fn attach_user_to_session(self: &Self, username: &str,
session: &ListenerSession) -> DResult<()> {
let username_l = username.to_lowercase();
self.pg_trans()?
.execute("INSERT INTO sendqueue (session, listener, message) \
SELECT current_session, current_listener, $1 FROM users \
WHERE username = $2 AND current_session IS NOT NULL \
AND current_listener IS NOT NULL",
&[&"Logged in from another session\r\n", &username_l]).await?;
self.pg_trans()?
.execute("UPDATE users SET current_session = $1, current_listener = $2 WHERE username = $3",
&[&session.session as &(dyn ToSql + Sync), &session.listener, &username_l]).await?;
Ok(())
}
pub async fn commit(mut self: Self) -> DResult<()> { pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));

View File

@ -11,8 +11,8 @@ pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) ->
Welcome to <red>BlastMud<reset> - a text-based post-apocalyptic \ Welcome to <red>BlastMud<reset> - a text-based post-apocalyptic \
game <bold>restricted to adults (18+)<reset>\r\n\ game <bold>restricted to adults (18+)<reset>\r\n\
Some commands to get you started:\r\n\ Some commands to get you started:\r\n\
\t<bold>register <lt>username> <lt>password> to register as a new user.\r\n\ \t<bold>register <lt>username> <lt>password> <lt>email><reset> to register as a new user.\r\n\
\t<bold>connect <lt>username> <lt>password><reset> to log in as an existing user.\r\n\ \t<bold>login <lt>username> <lt>password><reset> to log in as an existing user.\r\n\
\t<bold>help<reset> to learn more.\r\n\ \t<bold>help<reset> to learn more.\r\n\
[Please note BlastMud is still under development. You are welcome to play as we \ [Please note BlastMud is still under development. You are welcome to play as we \
develop it, but note it might still have bugs, unimplemented features, and \ develop it, but note it might still have bugs, unimplemented features, and \

View File

@ -14,6 +14,7 @@ mod quit;
mod less_explicit_mode; mod less_explicit_mode;
mod register; mod register;
mod agree; mod agree;
mod login;
pub struct VerbContext<'l> { pub struct VerbContext<'l> {
session: &'l ListenerSession, session: &'l ListenerSession,
@ -36,15 +37,9 @@ pub trait UserVerb {
pub type UResult<A> = Result<A, CommandHandlingError>; pub type UResult<A> = Result<A, CommandHandlingError>;
impl From<&str> for CommandHandlingError { impl<T> From<T> for CommandHandlingError where T: Into<Box<dyn std::error::Error + Send + Sync>> {
fn from(input: &str) -> CommandHandlingError { fn from(input: T) -> CommandHandlingError {
SystemError(Box::from(input)) SystemError(input.into())
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for CommandHandlingError {
fn from(input: Box<dyn std::error::Error + Send + Sync>) -> CommandHandlingError {
SystemError(input)
} }
} }
@ -66,6 +61,8 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! { static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"less_explicit_mode" => less_explicit_mode::VERB, "less_explicit_mode" => less_explicit_mode::VERB,
"register" => register::VERB, "register" => register::VERB,
"login" => login::VERB,
"connect" => login::VERB,
"agree" => agree::VERB "agree" => agree::VERB
}; };

View File

@ -1,7 +1,8 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult}; use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use crate::models::user::{User, UserTermData}; use crate::models::user::{User, UserTermData};
use async_trait::async_trait; use async_trait::async_trait;
use ansi_macro::ansi; use ansi_macro::ansi;
use chrono::Utc;
pub struct Verb; pub struct Verb;
@ -23,7 +24,7 @@ static REQUIRED_AGREEMENTS: [&str;4] = [
is personally identifying information, or is objectionable or abhorrent (including, without \ is personally identifying information, or is objectionable or abhorrent (including, without \
limitation, any content related to sexual violence, real or fictional children under 18, bestiality, \ limitation, any content related to sexual violence, real or fictional children under 18, bestiality, \
the promotion or glorification of proscribed drug use, or fetishes that involve degrading or \ the promotion or glorification of proscribed drug use, or fetishes that involve degrading or \
inflicting pain in someone for the enjoyment of others). I agree to defend, indemnify, and hold \ inflicting pain on someone for the enjoyment of others). I agree to defend, indemnify, and hold \
harmless the creators, staff, volunteers and contributors in any matter relating to content sent \ harmless the creators, staff, volunteers and contributors in any matter relating to content sent \
(or re-sent) by me, in any matter arising from the game sending content to me, and in any matter \ (or re-sent) by me, in any matter arising from the game sending content to me, and in any matter \
consequential to sharing my password, using an insecure password, or otherwise allowing or taking \ consequential to sharing my password, using an insecure password, or otherwise allowing or taking \
@ -68,7 +69,9 @@ fn first_outstanding_agreement(ctx: &VerbContext) -> UResult<Option<(String, Str
pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool> where 'b: 'a { pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool> where 'b: 'a {
match first_outstanding_agreement(ctx)? { match first_outstanding_agreement(ctx)? {
None => { None => {
user_mut(ctx)?.terms.terms_complete = true; let user = user_mut(ctx)?;
user.terms.terms_complete = true;
user.terms.last_presented_term = None;
Ok(true) Ok(true)
} }
Some((text, hash)) => { Some((text, hash)) => {
@ -77,9 +80,9 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U
user.terms.last_presented_term = Some(hash); user.terms.last_presented_term = Some(hash);
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!( ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!(
"Please review the following:\r\n\ "Please review the following:\r\n\
{}\r\n\ \t{}\r\n\
Type <bold>agree<reset> to accept. If you can't or don't agree, you \ Type <green><bold>agree<reset> to accept. If you can't or don't agree, you \
unfortunately can't play, so type <bold>quit<reset> to log off.\r\n"), unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"),
text))).await?; text))).await?;
Ok(false) Ok(false)
} }
@ -88,7 +91,24 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
let user = user_mut(ctx)?;
match user.terms.last_presented_term.as_ref() {
None => {
drop(user);
user_error("There was nothing pending your agreement.".to_owned())?;
}
Some(last_term) => {
user.terms.accepted_terms.insert(last_term.to_owned(), Utc::now());
drop(user);
if check_and_notify_accepts(ctx).await? {
ctx.trans.queue_for_session(ctx.session, Some(
ansi!("That was the last of the terms to agree to - welcome onboard!\r\n\
Hint: Try <bold>l<reset> to look around.\r\n"))).await?;
}
}
}
ctx.trans.save_user_model(ctx.user_dat.as_ref().unwrap()).await?;
Ok(()) Ok(())
} }
} }

View File

@ -20,19 +20,25 @@ static UNREGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map!
Topics of interest to unregistered users:\r\n\ Topics of interest to unregistered users:\r\n\
\t<bold>register<reset>\tLearn about the <bold>register<reset> command.\r\n\ \t<bold>register<reset>\tLearn about the <bold>register<reset> command.\r\n\
\t<bold>login<reset>\tLearn how to log in as an existing user.\r\n"), \t<bold>login<reset>\tLearn how to log in as an existing user.\r\n"),
"register" =>
ansi!("Registers a new user. You are allowed at most 5 at once.\r\n\
\t<bold>register <lt>username> <lt>password> <lt>email><reset>\r\n\
Email will be used to check you don't have too many accounts and \
in case you need to reset your password."),
"login" =>
ansi!("Logs in as an existing user.\r\n\
\t<bold>login <lt>username> <lt>password<reset>")
}; };
static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
"" => "" =>
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \ ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
commands can be used as a topicname.\r\n\ commands can be used as a topicname.\r\n\
Topics of interest to new users:\r\n\ Topics of interest:\r\n\
\t<bold>register<reset>\tLearn about the <bold>register<reset> command.\r\n\ \t<bold>newbie<reset>\tLearn the absolute basics."),
\t<bold>newbie<reset>\tLearn how to survive as a newb."), "newbie" =>
"<topicname>" => ansi!("So you've just landed in BlastMud, and want to know how to get started?\r\n\
ansi!("You are supposed to replace <lt>topicname> with the topic you want \ As we develop the game, this will eventually have some useful information for you!"),
to learn about. Example:\r\n\
\t<bold>help register<reset> will tell you about the register command.")
}; };
static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {

View File

@ -0,0 +1,36 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use async_trait::async_trait;
use tokio::time;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (username, password) = match remaining.split_whitespace().collect::<Vec<&str>>()[..] {
[] => user_error("Too few options to login".to_owned())?,
[username, password] => (username, password),
_ => user_error("Too many options to login".to_owned())?,
};
match ctx.trans.find_by_username(username).await? {
None => user_error("No such user.".to_owned())?,
Some(user) => {
time::sleep(time::Duration::from_secs(5)).await;
if !bcrypt::verify(password, &user.password_hash)? {
user_error("Invalid password.".to_owned())?
}
*ctx.user_dat = Some(user);
}
}
ctx.trans.attach_user_to_session(username, ctx.session).await?;
super::agree::check_and_notify_accepts(ctx).await?;
if let Some(user) = ctx.user_dat {
ctx.trans.save_user_model(user).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -4,23 +4,30 @@ use super::{user_error, parsing::parse_username};
use crate::models::{user::User, item::Item}; use crate::models::{user::User, item::Item};
use chrono::Utc; use chrono::Utc;
use ansi_macro::ansi; use ansi_macro::ansi;
use tokio::time;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (username, mut password) = match parse_username(remaining) { let (username, password, email) = match parse_username(remaining) {
Err(e) => user_error("Invalid username: ".to_owned() + e)?, Err(e) => user_error("Invalid username: ".to_owned() + e)?,
Ok(r) => r Ok((username, rest)) => {
match rest.split_whitespace().collect::<Vec<&str>>()[..] {
[password, email] => (username, password, email),
[] | [_] => user_error("Too few options to register - supply username, password, and email".to_owned())?,
_ => user_error("Too many options to register - supply username, password, and email".to_owned())?,
}
}
}; };
password = password.trim();
if ctx.trans.find_by_username(username).await?.is_some() { if ctx.trans.find_by_username(username).await?.is_some() {
user_error("Username already exists".to_owned())?; user_error("Username already exists".to_owned())?;
} }
if password.contains(" ") || password.contains("\t") { if password.len() < 6 {
user_error("To avoid future confusion, password can't contain spaces / tabs".to_owned())?;
} else if password.len() < 6 {
user_error("Password must be 6 characters long or longer".to_owned())?; user_error("Password must be 6 characters long or longer".to_owned())?;
} else if !validator::validate_email(email) {
user_error("Please supply a valid email in case you need to reset your password.".to_owned())?;
} }
let player_item_id = ctx.trans.create_item(&Item { let player_item_id = ctx.trans.create_item(&Item {
@ -30,10 +37,14 @@ impl UserVerb for Verb {
location: "room/chargen_room".to_owned(), location: "room/chargen_room".to_owned(),
..Item::default() ..Item::default()
}).await?; }).await?;
// Force a wait to protect against abuse.
time::sleep(time::Duration::from_secs(5)).await;
let password_hash = bcrypt::hash(password, 10).expect("hash not to fail"); let password_hash = bcrypt::hash(password, 10).expect("hash not to fail");
let user_dat = User { let user_dat = User {
username: username.to_owned(), username: username.to_owned(),
password_hash: password_hash.to_owned(), password_hash: password_hash.to_owned(),
email: email.to_owned(),
player_item_id, player_item_id,
registered_at: Some(Utc::now()), registered_at: Some(Utc::now()),
..User::default() ..User::default()

View File

@ -66,6 +66,7 @@ pub enum StatType {
pub struct User { pub struct User {
pub username: String, pub username: String,
pub password_hash: String, // bcrypted. pub password_hash: String, // bcrypted.
pub email: String,
pub player_item_id: i64, pub player_item_id: i64,
pub registered_at: Option<DateTime<Utc>>, pub registered_at: Option<DateTime<Utc>>,
pub banned_until: Option<DateTime<Utc>>, pub banned_until: Option<DateTime<Utc>>,
@ -106,6 +107,7 @@ impl Default for User {
User { User {
username: "unknown".to_owned(), username: "unknown".to_owned(),
password_hash: "unknown".to_owned(), password_hash: "unknown".to_owned(),
email: "unknown".to_owned(),
player_item_id: 0, player_item_id: 0,
registered_at: None, registered_at: None,
banned_until: None, banned_until: None,