From 536248864546aaa8189ed0adca1e7617895205f8 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Tue, 25 May 2021 15:05:35 +0000 Subject: [PATCH] Tack state onto user accounts. Make password optional. Everybody is a user! --- sqlx-data.json | 120 ++++++------------ src/commands/management.rs | 6 +- .../migrator/migrations/V8__user_state.rs | 21 +++ .../migrations/V9__nullable_password.rs | 21 +++ src/db/sqlite/users.rs | 17 ++- src/models.rs | 56 +++++++- 6 files changed, 141 insertions(+), 100 deletions(-) create mode 100644 src/db/sqlite/migrator/migrations/V8__user_state.rs create mode 100644 src/db/sqlite/migrator/migrations/V9__nullable_password.rs diff --git a/sqlx-data.json b/sqlx-data.json index 507e00b..af0b0ea 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -18,6 +18,42 @@ ] } }, + "2eadaecbb9b743592cd8498ece3aeea97560fe8721ba085407456c99cb99b02d": { + "query": "SELECT\n a.user_id as \"username\", a.password,\n s.active_room as \"active_room: _\",\n s.account_status as \"account_status: _\"\n FROM accounts a\n JOIN user_state s on a.user_id = s.user_id\n WHERE a.user_id = ?", + "describe": { + "columns": [ + { + "name": "username", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "password", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "active_room: _", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "account_status: _", + "ordinal": 3, + "type_info": "Null" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true, + true, + false + ] + } + }, "59313c67900a1a9399389720b522e572f181ae503559cd2b49d6305acb9e2207": { "query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ? AND user_id = ?", "describe": { @@ -60,30 +96,6 @@ ] } }, - "64e137107139c56a43f7041db933671c210df4fa5110fe481d191fd63b2d3aeb": { - "query": "SELECT user_id, password FROM accounts\n WHERE user_id = ?", - "describe": { - "columns": [ - { - "name": "user_id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "password", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false - ] - } - }, "711d222911c1258365a6a0de1fe00eeec4686fd3589e976e225ad599e7cfc75d": { "query": "SELECT count(*) as \"count: i32\" FROM user_variables\n WHERE room_id = ? and user_id = ?", "describe": { @@ -102,66 +114,6 @@ ] } }, - "7248c8ae30bbe4bc5866e80cc277312c7f8cb9af5a8801fd8eaf178fd99eae18": { - "query": "SELECT room_id FROM room_users\n WHERE username = ?", - "describe": { - "columns": [ - { - "name": "room_id", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - } - }, - "97f5d58f62baca51efd8c295ca6737d1240923c69c973621cd0a718ac9eed99f": { - "query": "SELECT room_id, room_name FROM room_info\n WHERE room_id = ?", - "describe": { - "columns": [ - { - "name": "room_id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "room_name", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false - ] - } - }, - "b302d586e5ac4c72c2970361ea5a5936c0b8c6dad10033c626a0ce0404cadb25": { - "query": "SELECT username FROM room_users\n WHERE room_id = ?", - "describe": { - "columns": [ - { - "name": "username", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - } - }, "bba0fc255e7c30d1d2d9468c68ba38db6e8a13be035aa1152933ba9247b14f8c": { "query": "SELECT event_id FROM room_events\n WHERE room_id = ? AND event_id = ?", "describe": { diff --git a/src/commands/management.rs b/src/commands/management.rs index a29b040..6d4c68c 100644 --- a/src/commands/management.rs +++ b/src/commands/management.rs @@ -3,7 +3,7 @@ use crate::context::Context; use crate::db::Users; use crate::error::BotError::{AccountDoesNotExist, AuthenticationError, PasswordCreationError}; use crate::logic::hash_password; -use crate::models::User; +use crate::models::{AccountStatus, User}; use async_trait::async_trait; pub struct RegisterCommand(pub String); @@ -22,7 +22,9 @@ impl Command for RegisterCommand { let pw_hash = hash_password(&self.0).map_err(|e| PasswordCreationError(e))?; let user = User { username: ctx.username.to_owned(), - password: pw_hash, + password: Some(pw_hash), + account_status: AccountStatus::Registered, + ..Default::default() }; ctx.db.upsert_user(&user).await?; diff --git a/src/db/sqlite/migrator/migrations/V8__user_state.rs b/src/db/sqlite/migrator/migrations/V8__user_state.rs new file mode 100644 index 0000000..5fbf6bc --- /dev/null +++ b/src/db/sqlite/migrator/migrations/V8__user_state.rs @@ -0,0 +1,21 @@ +use barrel::backend::Sqlite; +use barrel::{types, types::Type, Migration}; + +fn primary_uuid() -> Type { + types::text().unique(true).primary(true).nullable(false) +} + +pub fn migration() -> String { + let status_enum = + r#"CHECK(account_status IN ('not_registered', 'registered', 'awaiting_activation'))"#; + let mut m = Migration::new(); + + // Keep track of contextual user state. + m.create_table("user_state", move |t| { + t.add_column("user_id", primary_uuid()); + t.add_column("active_room", types::text().nullable(true)); + t.add_column("account_status", types::custom(status_enum).nullable(false)); + }); + + m.make::() +} diff --git a/src/db/sqlite/migrator/migrations/V9__nullable_password.rs b/src/db/sqlite/migrator/migrations/V9__nullable_password.rs new file mode 100644 index 0000000..751459f --- /dev/null +++ b/src/db/sqlite/migrator/migrations/V9__nullable_password.rs @@ -0,0 +1,21 @@ +use barrel::backend::Sqlite; +use barrel::{types, types::Type, Migration}; + +fn primary_uuid() -> Type { + types::text().unique(true).primary(true).nullable(false) +} + +pub fn migration() -> String { + // sqlite does really support alter column, and barrel does not + // implement the required workaround, so we do it ourselves! + r#" + CREATE TABLE IF NOT EXISTS "accounts2" ( + "user_id" TEXT PRIMARY KEY NOT NULL UNIQUE, + "password" TEXT NULL + ); + INSERT INTO accounts2 select * from accounts; + DROP TABLE accounts; + ALTER TABLE accounts2 RENAME TO accounts; + "# + .to_string() +} diff --git a/src/db/sqlite/users.rs b/src/db/sqlite/users.rs index 23b1dac..91696c9 100644 --- a/src/db/sqlite/users.rs +++ b/src/db/sqlite/users.rs @@ -30,18 +30,21 @@ impl Users for Database { } async fn get_user(&self, username: &str) -> Result, DataError> { - let user_row = sqlx::query!( - r#"SELECT user_id, password FROM accounts - WHERE user_id = ?"#, + let user_row = sqlx::query_as!( + User, + r#"SELECT + a.user_id as "username", a.password, + s.active_room as "active_room: _", + s.account_status as "account_status: _" + FROM accounts a + JOIN user_state s on a.user_id = s.user_id + WHERE a.user_id = ?"#, username ) .fetch_optional(&self.conn) .await?; - Ok(user_row.map(|u| User { - username: u.user_id, - password: u.password, - })) + Ok(user_row) } async fn authenticate_user( diff --git a/src/models.rs b/src/models.rs index ea22071..fc5b0a8 100644 --- a/src/models.rs +++ b/src/models.rs @@ -7,15 +7,43 @@ pub struct RoomInfo { pub room_name: String, } -#[derive(Eq, PartialEq, Debug)] +#[derive(Eq, PartialEq, Debug, sqlx::Type)] +#[sqlx(rename_all = "snake_case")] +pub enum AccountStatus { + /// User is not registered, which means the "account" only exists + /// for state management in the bot. No privileged actions + /// possible. + NotRegistered, + + /// User account is fully registered, either via Matrix directly, + /// or a web UI sign-up. + Registered, + + /// Account is awaiting activation with a registration + /// code. Account cannot do privileged actions yet. + AwaitingActivation, +} + +impl Default for AccountStatus { + fn default() -> Self { + AccountStatus::NotRegistered + } +} + +#[derive(Eq, PartialEq, Debug, Default)] pub struct User { pub username: String, - pub password: String, + pub password: Option, + pub active_room: Option, + pub account_status: AccountStatus, } impl User { pub fn verify_password(&self, raw_password: &str) -> bool { - argon2::verify_encoded(&self.password, raw_password.as_bytes()).unwrap_or(false) + self.password + .as_ref() + .and_then(|p| argon2::verify_encoded(p, raw_password.as_bytes()).ok()) + .unwrap_or(false) } } @@ -26,8 +54,10 @@ mod tests { #[test] fn verify_password_passes_with_correct_password() { let user = User { - username: "myuser".to_string(), - password: crate::logic::hash_password("mypassword").expect("Password hashing error!"), + password: Some( + crate::logic::hash_password("mypassword").expect("Password hashing error!"), + ), + ..Default::default() }; assert_eq!(user.verify_password("mypassword"), true); @@ -36,8 +66,20 @@ mod tests { #[test] fn verify_password_fails_with_wrong_password() { let user = User { - username: "myuser".to_string(), - password: crate::logic::hash_password("mypassword").expect("Password hashing error!"), + password: Some( + crate::logic::hash_password("mypassword").expect("Password hashing error!"), + ), + ..Default::default() + }; + + assert_eq!(user.verify_password("wrong-password"), false); + } + + #[test] + fn verify_password_fails_with_no_password() { + let user = User { + password: None, + ..Default::default() }; assert_eq!(user.verify_password("wrong-password"), false);