Get login working correctly.
This commit is contained in:
parent
16bd49f160
commit
672eb1ee75
61
Cargo.lock
generated
61
Cargo.lock
generated
@ -8,6 +8,15 @@ version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
@ -119,6 +128,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -692,6 +702,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
@ -785,6 +806,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.5"
|
||||
@ -1191,6 +1218,23 @@ dependencies = [
|
||||
"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]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
@ -1792,7 +1836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.3.0",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
@ -1812,6 +1856,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
@ -31,3 +31,4 @@ nom = "7.1.1"
|
||||
ouroboros = "0.15.5"
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
bcrypt = "0.13.0"
|
||||
validator = "0.16.0"
|
||||
|
@ -9,6 +9,7 @@ use tokio_postgres::NoTls;
|
||||
use crate::message_handler::ListenerSession;
|
||||
use crate::DResult;
|
||||
use crate::models::{session::Session, user::User, item::Item};
|
||||
use tokio_postgres::types::ToSql;
|
||||
|
||||
use serde_json;
|
||||
use futures::FutureExt;
|
||||
@ -226,6 +227,21 @@ impl DBTrans {
|
||||
&details.username.to_lowercase()]).await?;
|
||||
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<()> {
|
||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||
|
@ -11,8 +11,8 @@ pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) ->
|
||||
Welcome to <red>BlastMud<reset> - a text-based post-apocalyptic \
|
||||
game <bold>restricted to adults (18+)<reset>\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>connect <lt>username> <lt>password><reset> to log in as an existing user.\r\n\
|
||||
\t<bold>register <lt>username> <lt>password> <lt>email><reset> to register as a new 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\
|
||||
[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 \
|
||||
|
@ -14,6 +14,7 @@ mod quit;
|
||||
mod less_explicit_mode;
|
||||
mod register;
|
||||
mod agree;
|
||||
mod login;
|
||||
|
||||
pub struct VerbContext<'l> {
|
||||
session: &'l ListenerSession,
|
||||
@ -36,15 +37,9 @@ pub trait UserVerb {
|
||||
pub type UResult<A> = Result<A, CommandHandlingError>;
|
||||
|
||||
|
||||
impl From<&str> for CommandHandlingError {
|
||||
fn from(input: &str) -> CommandHandlingError {
|
||||
SystemError(Box::from(input))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error + Send + Sync>> for CommandHandlingError {
|
||||
fn from(input: Box<dyn std::error::Error + Send + Sync>) -> CommandHandlingError {
|
||||
SystemError(input)
|
||||
impl<T> From<T> for CommandHandlingError where T: Into<Box<dyn std::error::Error + Send + Sync>> {
|
||||
fn from(input: T) -> CommandHandlingError {
|
||||
SystemError(input.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,6 +61,8 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"less_explicit_mode" => less_explicit_mode::VERB,
|
||||
"register" => register::VERB,
|
||||
"login" => login::VERB,
|
||||
"connect" => login::VERB,
|
||||
"agree" => agree::VERB
|
||||
};
|
||||
|
||||
|
@ -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 async_trait::async_trait;
|
||||
use ansi_macro::ansi;
|
||||
use chrono::Utc;
|
||||
|
||||
pub struct Verb;
|
||||
|
||||
@ -23,7 +24,7 @@ static REQUIRED_AGREEMENTS: [&str;4] = [
|
||||
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, \
|
||||
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 \
|
||||
(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 \
|
||||
@ -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 {
|
||||
match first_outstanding_agreement(ctx)? {
|
||||
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)
|
||||
}
|
||||
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);
|
||||
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!(
|
||||
"Please review the following:\r\n\
|
||||
{}\r\n\
|
||||
Type <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"),
|
||||
\t{}\r\n\
|
||||
Type <green><bold>agree<reset> to accept. If you can't or don't agree, you \
|
||||
unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"),
|
||||
text))).await?;
|
||||
Ok(false)
|
||||
}
|
||||
@ -88,7 +91,24 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U
|
||||
|
||||
#[async_trait]
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -20,19 +20,25 @@ static UNREGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map!
|
||||
Topics of interest to unregistered users:\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"),
|
||||
"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! {
|
||||
"" =>
|
||||
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
|
||||
commands can be used as a topicname.\r\n\
|
||||
Topics of interest to new users:\r\n\
|
||||
\t<bold>register<reset>\tLearn about the <bold>register<reset> command.\r\n\
|
||||
\t<bold>newbie<reset>\tLearn how to survive as a newb."),
|
||||
"<topicname>" =>
|
||||
ansi!("You are supposed to replace <lt>topicname> with the topic you want \
|
||||
to learn about. Example:\r\n\
|
||||
\t<bold>help register<reset> will tell you about the register command.")
|
||||
Topics of interest:\r\n\
|
||||
\t<bold>newbie<reset>\tLearn the absolute basics."),
|
||||
"newbie" =>
|
||||
ansi!("So you've just landed in BlastMud, and want to know how to get started?\r\n\
|
||||
As we develop the game, this will eventually have some useful information for you!"),
|
||||
};
|
||||
|
||||
static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
|
||||
|
36
blastmud_game/src/message_handler/user_commands/login.rs
Normal file
36
blastmud_game/src/message_handler/user_commands/login.rs
Normal 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;
|
@ -4,23 +4,30 @@ use super::{user_error, parsing::parse_username};
|
||||
use crate::models::{user::User, item::Item};
|
||||
use chrono::Utc;
|
||||
use ansi_macro::ansi;
|
||||
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, mut password) = match parse_username(remaining) {
|
||||
let (username, password, email) = match parse_username(remaining) {
|
||||
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() {
|
||||
user_error("Username already exists".to_owned())?;
|
||||
}
|
||||
if password.contains(" ") || password.contains("\t") {
|
||||
user_error("To avoid future confusion, password can't contain spaces / tabs".to_owned())?;
|
||||
} else if password.len() < 6 {
|
||||
if password.len() < 6 {
|
||||
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 {
|
||||
@ -30,10 +37,14 @@ impl UserVerb for Verb {
|
||||
location: "room/chargen_room".to_owned(),
|
||||
..Item::default()
|
||||
}).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 user_dat = User {
|
||||
username: username.to_owned(),
|
||||
password_hash: password_hash.to_owned(),
|
||||
email: email.to_owned(),
|
||||
player_item_id,
|
||||
registered_at: Some(Utc::now()),
|
||||
..User::default()
|
||||
|
@ -66,6 +66,7 @@ pub enum StatType {
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password_hash: String, // bcrypted.
|
||||
pub email: String,
|
||||
pub player_item_id: i64,
|
||||
pub registered_at: Option<DateTime<Utc>>,
|
||||
pub banned_until: Option<DateTime<Utc>>,
|
||||
@ -106,6 +107,7 @@ impl Default for User {
|
||||
User {
|
||||
username: "unknown".to_owned(),
|
||||
password_hash: "unknown".to_owned(),
|
||||
email: "unknown".to_owned(),
|
||||
player_item_id: 0,
|
||||
registered_at: None,
|
||||
banned_until: None,
|
||||
|
Loading…
Reference in New Issue
Block a user