Tack state onto user accounts. Make password optional. Everybody is a user!
This commit is contained in:
parent
849a1b6a14
commit
5362488645
120
sqlx-data.json
120
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": {
|
"59313c67900a1a9399389720b522e572f181ae503559cd2b49d6305acb9e2207": {
|
||||||
"query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ? AND user_id = ?",
|
"query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ? AND user_id = ?",
|
||||||
"describe": {
|
"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": {
|
"711d222911c1258365a6a0de1fe00eeec4686fd3589e976e225ad599e7cfc75d": {
|
||||||
"query": "SELECT count(*) as \"count: i32\" FROM user_variables\n WHERE room_id = ? and user_id = ?",
|
"query": "SELECT count(*) as \"count: i32\" FROM user_variables\n WHERE room_id = ? and user_id = ?",
|
||||||
"describe": {
|
"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": {
|
"bba0fc255e7c30d1d2d9468c68ba38db6e8a13be035aa1152933ba9247b14f8c": {
|
||||||
"query": "SELECT event_id FROM room_events\n WHERE room_id = ? AND event_id = ?",
|
"query": "SELECT event_id FROM room_events\n WHERE room_id = ? AND event_id = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::context::Context;
|
||||||
use crate::db::Users;
|
use crate::db::Users;
|
||||||
use crate::error::BotError::{AccountDoesNotExist, AuthenticationError, PasswordCreationError};
|
use crate::error::BotError::{AccountDoesNotExist, AuthenticationError, PasswordCreationError};
|
||||||
use crate::logic::hash_password;
|
use crate::logic::hash_password;
|
||||||
use crate::models::User;
|
use crate::models::{AccountStatus, User};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
pub struct RegisterCommand(pub String);
|
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 pw_hash = hash_password(&self.0).map_err(|e| PasswordCreationError(e))?;
|
||||||
let user = User {
|
let user = User {
|
||||||
username: ctx.username.to_owned(),
|
username: ctx.username.to_owned(),
|
||||||
password: pw_hash,
|
password: Some(pw_hash),
|
||||||
|
account_status: AccountStatus::Registered,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.db.upsert_user(&user).await?;
|
ctx.db.upsert_user(&user).await?;
|
||||||
|
|
|
@ -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::<Sqlite>()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -30,18 +30,21 @@ impl Users for Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(&self, username: &str) -> Result<Option<User>, DataError> {
|
async fn get_user(&self, username: &str) -> Result<Option<User>, DataError> {
|
||||||
let user_row = sqlx::query!(
|
let user_row = sqlx::query_as!(
|
||||||
r#"SELECT user_id, password FROM accounts
|
User,
|
||||||
WHERE user_id = ?"#,
|
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
|
username
|
||||||
)
|
)
|
||||||
.fetch_optional(&self.conn)
|
.fetch_optional(&self.conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(user_row.map(|u| User {
|
Ok(user_row)
|
||||||
username: u.user_id,
|
|
||||||
password: u.password,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate_user(
|
async fn authenticate_user(
|
||||||
|
|
|
@ -7,15 +7,43 @@ pub struct RoomInfo {
|
||||||
pub room_name: String,
|
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 struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: Option<String>,
|
||||||
|
pub active_room: Option<String>,
|
||||||
|
pub account_status: AccountStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn verify_password(&self, raw_password: &str) -> bool {
|
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]
|
#[test]
|
||||||
fn verify_password_passes_with_correct_password() {
|
fn verify_password_passes_with_correct_password() {
|
||||||
let user = User {
|
let user = User {
|
||||||
username: "myuser".to_string(),
|
password: Some(
|
||||||
password: crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(user.verify_password("mypassword"), true);
|
assert_eq!(user.verify_password("mypassword"), true);
|
||||||
|
@ -36,8 +66,20 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_password_fails_with_wrong_password() {
|
fn verify_password_fails_with_wrong_password() {
|
||||||
let user = User {
|
let user = User {
|
||||||
username: "myuser".to_string(),
|
password: Some(
|
||||||
password: crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
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);
|
assert_eq!(user.verify_password("wrong-password"), false);
|
||||||
|
|
Loading…
Reference in New Issue