Separate registering and linking accounts.

Can register an account with the bot to manage variables and stuff in
private room, and then separately "link" it with a password, which
makes it available to anything using the bot API (aka web app). Can
also unlink and unregister. Check command no longer validates
password. It just checks and reports your account status.
This commit is contained in:
projectmoon 2021-05-26 15:28:59 +00:00
parent 495df13fe6
commit 5b3d174edc
6 changed files with 124 additions and 21 deletions

View File

@ -1,13 +1,13 @@
use super::{Command, Execution, ExecutionResult}; use super::{Command, Execution, ExecutionError, ExecutionResult};
use crate::db::Users; use crate::db::Users;
use crate::error::BotError::{AccountDoesNotExist, AuthenticationError, PasswordCreationError}; use crate::error::BotError::{AccountDoesNotExist, PasswordCreationError};
use crate::logic::hash_password; use crate::logic::hash_password;
use crate::models::{AccountStatus, User}; use crate::models::{AccountStatus, User};
use crate::{context::Context, error::BotError}; use crate::{context::Context, error::BotError};
use async_trait::async_trait; use async_trait::async_trait;
use std::convert::{Into, TryFrom}; use std::convert::{Into, TryFrom};
pub struct RegisterCommand(pub String); pub struct RegisterCommand;
impl From<RegisterCommand> for Box<dyn Command> { impl From<RegisterCommand> for Box<dyn Command> {
fn from(cmd: RegisterCommand) -> Self { fn from(cmd: RegisterCommand) -> Self {
@ -18,8 +18,8 @@ impl From<RegisterCommand> for Box<dyn Command> {
impl TryFrom<&str> for RegisterCommand { impl TryFrom<&str> for RegisterCommand {
type Error = BotError; type Error = BotError;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(_: &str) -> Result<Self, Self::Error> {
Ok(RegisterCommand(value.to_owned())) Ok(RegisterCommand)
} }
} }
@ -34,24 +34,114 @@ impl Command for RegisterCommand {
} }
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult { async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
let pw_hash = hash_password(&self.0).map_err(|e| PasswordCreationError(e))?; if let Some(_) = ctx.db.get_user(ctx.username).await? {
return Err(ExecutionError(BotError::AccountAlreadyExists));
}
let user = User { let user = User {
username: ctx.username.to_owned(), username: ctx.username.to_owned(),
password: Some(pw_hash), password: None,
account_status: AccountStatus::Registered, account_status: AccountStatus::Registered,
..Default::default() ..Default::default()
}; };
ctx.db.upsert_user(&user).await?; ctx.db.upsert_user(&user).await?;
Execution::success(format!( Execution::success(format!(
"User account registered/updated. Please log in to external applications \ "User account {} registered for bot commands.",
with username {} and the password you set.",
ctx.username ctx.username
)) ))
} }
} }
pub struct CheckCommand(pub String); pub struct UnlinkCommand(pub String);
impl From<UnlinkCommand> for Box<dyn Command> {
fn from(cmd: UnlinkCommand) -> Self {
Box::new(cmd)
}
}
impl TryFrom<&str> for UnlinkCommand {
type Error = BotError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(UnlinkCommand(value.to_owned()))
}
}
#[async_trait]
impl Command for UnlinkCommand {
fn name(&self) -> &'static str {
"unlink user accountx from external applications"
}
fn is_secure(&self) -> bool {
true
}
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
let mut user = ctx
.db
.get_user(&ctx.username)
.await?
.ok_or(BotError::AccountDoesNotExist)?;
user.password = None;
ctx.db.upsert_user(&user).await?;
Execution::success(format!(
"Accounted {} is now inaccessible to external applications.",
ctx.username
))
}
}
pub struct LinkCommand(pub String);
impl From<LinkCommand> for Box<dyn Command> {
fn from(cmd: LinkCommand) -> Self {
Box::new(cmd)
}
}
impl TryFrom<&str> for LinkCommand {
type Error = BotError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(LinkCommand(value.to_owned()))
}
}
#[async_trait]
impl Command for LinkCommand {
fn name(&self) -> &'static str {
"link user account to external applications"
}
fn is_secure(&self) -> bool {
true
}
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
let mut user = ctx
.db
.get_user(&ctx.username)
.await?
.ok_or(BotError::AccountDoesNotExist)?;
let pw_hash = hash_password(&self.0).map_err(|e| PasswordCreationError(e))?;
user.password = Some(pw_hash);
ctx.db.upsert_user(&user).await?;
Execution::success(format!(
"Accounted now available for external use. Please log in to \
external applications with username {} and the password you set.",
ctx.username
))
}
}
pub struct CheckCommand;
impl From<CheckCommand> for Box<dyn Command> { impl From<CheckCommand> for Box<dyn Command> {
fn from(cmd: CheckCommand) -> Self { fn from(cmd: CheckCommand) -> Self {
@ -62,15 +152,15 @@ impl From<CheckCommand> for Box<dyn Command> {
impl TryFrom<&str> for CheckCommand { impl TryFrom<&str> for CheckCommand {
type Error = BotError; type Error = BotError;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(_: &str) -> Result<Self, Self::Error> {
Ok(CheckCommand(value.to_owned())) Ok(CheckCommand)
} }
} }
#[async_trait] #[async_trait]
impl Command for CheckCommand { impl Command for CheckCommand {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"check user password" "check user account status"
} }
fn is_secure(&self) -> bool { fn is_secure(&self) -> bool {
@ -78,11 +168,20 @@ impl Command for CheckCommand {
} }
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult { async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
let user = ctx.db.authenticate_user(&ctx.username, &self.0).await?; let user = ctx.db.get_user(&ctx.username).await?;
match user { match user {
Some(_) => Execution::success("Password is correct!".to_string()), Some(user) => match user.password {
None => Err(AuthenticationError.into()), Some(_) => Execution::success(
"Account exists, and is available to external applications with a password. If you forgot your password, change it with !link.".to_string(),
),
None => Execution::success(
"Account exists, but is not available to external applications.".to_string(),
),
},
None => Execution::success(
"No account registered. Only simple commands in public rooms are available.".to_string(),
),
} }
} }
} }

View File

@ -209,7 +209,7 @@ mod tests {
message_body: "!notacommand", message_body: "!notacommand",
}; };
let cmd = RegisterCommand("".to_owned()); let cmd = RegisterCommand;
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true); assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
} }
@ -275,7 +275,7 @@ mod tests {
message_body: "!notacommand", message_body: "!notacommand",
}; };
let cmd = RegisterCommand("".to_owned()); let cmd = RegisterCommand;
assert_eq!(execution_allowed(&cmd, &ctx).is_err(), true); assert_eq!(execution_allowed(&cmd, &ctx).is_err(), true);
} }

View File

@ -7,7 +7,7 @@ use crate::commands::{
basic_rolling::RollCommand, basic_rolling::RollCommand,
cofd::PoolRollCommand, cofd::PoolRollCommand,
cthulhu::{CthAdvanceRoll, CthRoll}, cthulhu::{CthAdvanceRoll, CthRoll},
management::{CheckCommand, RegisterCommand, UnregisterCommand}, management::{CheckCommand, LinkCommand, RegisterCommand, UnlinkCommand, UnregisterCommand},
misc::HelpCommand, misc::HelpCommand,
variables::{ variables::{
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand, DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
@ -86,6 +86,8 @@ pub fn parse_command(input: &str) -> Result<Box<dyn Command>, BotError> {
"cthadv" | "ctharoll" => convert_to!(CthAdvanceRoll, cmd_input), "cthadv" | "ctharoll" => convert_to!(CthAdvanceRoll, cmd_input),
"help" => convert_to!(HelpCommand, cmd_input), "help" => convert_to!(HelpCommand, cmd_input),
"register" => convert_to!(RegisterCommand, cmd_input), "register" => convert_to!(RegisterCommand, cmd_input),
"link" => convert_to!(LinkCommand, cmd_input),
"unlink" => convert_to!(UnlinkCommand, cmd_input),
"check" => convert_to!(CheckCommand, cmd_input), "check" => convert_to!(CheckCommand, cmd_input),
"unregister" => convert_to!(UnregisterCommand, cmd_input), "unregister" => convert_to!(UnregisterCommand, cmd_input),
_ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()), _ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()),

View File

@ -1,6 +1,6 @@
use crate::db::sqlite::Database; use crate::db::sqlite::Database;
use crate::error::BotError; use crate::error::BotError;
use crate::models::{Account, User}; use crate::models::Account;
use matrix_sdk::identifiers::{RoomId, UserId}; use matrix_sdk::identifiers::{RoomId, UserId};
use matrix_sdk::room::Joined; use matrix_sdk::room::Joined;
use matrix_sdk::Client; use matrix_sdk::Client;

View File

@ -3,7 +3,6 @@ use crate::db::{errors::DataError, Users};
use crate::error::BotError; use crate::error::BotError;
use crate::models::User; use crate::models::User;
use async_trait::async_trait; use async_trait::async_trait;
use log::info;
#[async_trait] #[async_trait]
impl Users for Database { impl Users for Database {

View File

@ -87,6 +87,9 @@ pub enum BotError {
#[error("user account does not exist, try registering")] #[error("user account does not exist, try registering")]
AccountDoesNotExist, AccountDoesNotExist,
#[error("user account already exists")]
AccountAlreadyExists,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]