Add bcrypt password hashing and user gender handling in yourchat2
Integrated bcrypt for password verification and updated user profile management to include gender and rights handling. Enhanced room access validation to consider gender restrictions and user rights. Updated database queries and structures to support new fields, ensuring compatibility with existing functionalities.
This commit is contained in:
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -53,6 +53,19 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523ab528ce3a7ada6597f8ccf5bd8d85ebe26d5edf311cad4d1d3cfb2d357ac6"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blowfish",
|
||||
"getrandom 0.4.2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@@ -68,6 +81,16 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -1602,6 +1625,7 @@ dependencies = [
|
||||
name = "yourchat2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"openssl",
|
||||
|
||||
@@ -16,3 +16,4 @@ hex = "0.4"
|
||||
scrypt = "0.11"
|
||||
tokio-rustls = "0.26"
|
||||
rustls-pemfile = "2"
|
||||
bcrypt = "0.19.0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::types::{ChatState, ClientId, Command, RoomMeta, ServerConfig};
|
||||
use bcrypt::verify as bcrypt_verify;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -169,7 +170,9 @@ async fn handle_init_command(
|
||||
guard.room_meta.get(&resolved_room_name),
|
||||
&profile.display_name,
|
||||
profile.falukant_user_id,
|
||||
profile.gender_id,
|
||||
profile.age,
|
||||
&profile.right_type_ids,
|
||||
&room_password,
|
||||
) {
|
||||
drop(guard);
|
||||
@@ -189,8 +192,10 @@ async fn handle_init_command(
|
||||
client.color = profile.color.clone();
|
||||
client.falukant_user_id = profile.falukant_user_id;
|
||||
client.chat_user_id = profile.chat_user_id;
|
||||
client.gender_id = profile.gender_id;
|
||||
client.age = profile.age;
|
||||
client.rights = profile.rights.clone();
|
||||
client.right_type_ids = profile.right_type_ids.clone();
|
||||
client.logged_in = true;
|
||||
client.room = resolved_room_name.clone();
|
||||
|
||||
@@ -271,17 +276,25 @@ async fn handle_join_command(
|
||||
send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await;
|
||||
return;
|
||||
};
|
||||
let (name_for_check, falukant_user_id, age) = {
|
||||
let (name_for_check, falukant_user_id, gender_id, age, right_type_ids) = {
|
||||
let Some(client) = guard.clients.get(&client_id) else {
|
||||
return;
|
||||
};
|
||||
(client.user_name.clone(), client.falukant_user_id, client.age)
|
||||
(
|
||||
client.user_name.clone(),
|
||||
client.falukant_user_id,
|
||||
client.gender_id,
|
||||
client.age,
|
||||
client.right_type_ids.clone(),
|
||||
)
|
||||
};
|
||||
if !room_access_allowed(
|
||||
guard.room_meta.get(&resolved_room),
|
||||
&name_for_check,
|
||||
falukant_user_id,
|
||||
gender_id,
|
||||
age,
|
||||
&right_type_ids,
|
||||
&password,
|
||||
) {
|
||||
drop(guard);
|
||||
@@ -770,45 +783,82 @@ fn room_access_allowed(
|
||||
room_meta: Option<&RoomMeta>,
|
||||
_user_name: &str,
|
||||
falukant_user_id: Option<i32>,
|
||||
user_gender_id: Option<i32>,
|
||||
age: Option<i32>,
|
||||
user_right_type_ids: &std::collections::HashSet<i32>,
|
||||
provided_password: &str,
|
||||
) -> bool {
|
||||
let Some(room) = room_meta else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(required_gender) = room.gender_restriction_id {
|
||||
if required_gender > 0 && user_gender_id != Some(required_gender) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(room_password) = &room.password {
|
||||
if !room_password.is_empty() && room_password != provided_password {
|
||||
if !room_password.is_empty() && !password_matches(room_password, provided_password) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(min_age) = room.min_age {
|
||||
if min_age >= 18 {
|
||||
if min_age > 0 {
|
||||
let Some(fid) = falukant_user_id else {
|
||||
return false;
|
||||
};
|
||||
if fid <= 0 {
|
||||
return false;
|
||||
}
|
||||
if let Some(user_age) = age {
|
||||
if user_age < min_age {
|
||||
return false;
|
||||
}
|
||||
let Some(user_age) = age else {
|
||||
return false;
|
||||
};
|
||||
if user_age < min_age {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(max_age) = room.max_age {
|
||||
if let Some(user_age) = age {
|
||||
if max_age > 0 {
|
||||
let Some(user_age) = age else {
|
||||
return false;
|
||||
};
|
||||
if user_age > max_age {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(required_right) = room.required_user_right_id {
|
||||
if required_right > 0 && !user_right_type_ids.contains(&required_right) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn password_matches(stored_password_or_hash: &str, provided_password: &str) -> bool {
|
||||
if stored_password_or_hash.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if stored_password_or_hash == provided_password {
|
||||
return true;
|
||||
}
|
||||
if provided_password.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if stored_password_or_hash.starts_with("$2a$")
|
||||
|| stored_password_or_hash.starts_with("$2b$")
|
||||
|| stored_password_or_hash.starts_with("$2x$")
|
||||
|| stored_password_or_hash.starts_with("$2y$")
|
||||
{
|
||||
return bcrypt_verify(provided_password, stored_password_or_hash).unwrap_or(false);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn resolve_room_name(state: &ChatState, requested: &str) -> Option<String> {
|
||||
if state.room_meta.contains_key(requested) {
|
||||
return Some(requested.to_string());
|
||||
|
||||
163
src/db.rs
163
src/db.rs
@@ -42,8 +42,10 @@ pub async fn load_user_profile(user_name: &str, config: &ServerConfig) -> Result
|
||||
color: None,
|
||||
falukant_user_id: None,
|
||||
chat_user_id: None,
|
||||
gender_id: None,
|
||||
age: None,
|
||||
rights: HashSet::new(),
|
||||
right_type_ids: HashSet::new(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -88,27 +90,28 @@ pub async fn load_user_profile(user_name: &str, config: &ServerConfig) -> Result
|
||||
)
|
||||
};
|
||||
|
||||
let rights_rows = client
|
||||
.query(
|
||||
"SELECT r.tr FROM chat.user_rights ur JOIN chat.rights r ON ur.chat_right_id = r.id WHERE ur.chat_user_id = $1",
|
||||
&[&chat_user_id],
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "db_error")?;
|
||||
let mut rights = HashSet::new();
|
||||
for row in rights_rows {
|
||||
rights.insert(row.get::<_, String>("tr"));
|
||||
if let Ok(chat_rights) = load_chat_rights(client.clone(), chat_user_id).await {
|
||||
rights.extend(chat_rights);
|
||||
}
|
||||
let mut right_type_ids = HashSet::new();
|
||||
if let Ok((community_rights, community_right_ids)) = load_community_rights(client.clone(), falukant_user_id).await {
|
||||
rights.extend(community_rights);
|
||||
right_type_ids.extend(community_right_ids);
|
||||
}
|
||||
|
||||
let age = load_user_age(client.clone(), falukant_user_id).await;
|
||||
let gender_id = load_user_gender(client.clone(), falukant_user_id).await;
|
||||
|
||||
Ok(UserProfile {
|
||||
display_name,
|
||||
color,
|
||||
falukant_user_id: Some(falukant_user_id),
|
||||
chat_user_id: Some(chat_user_id),
|
||||
gender_id,
|
||||
age,
|
||||
rights,
|
||||
right_type_ids,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -152,21 +155,36 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result<Vec<RoomMeta>, &
|
||||
}]);
|
||||
};
|
||||
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT r.title, r.password_hash, r.is_public, r.owner_id, r.min_age, r.max_age
|
||||
FROM chat.room r
|
||||
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id",
|
||||
&[],
|
||||
)
|
||||
let has_gender_restriction = column_exists(client.clone(), "chat", "room", "gender_restriction_id")
|
||||
.await
|
||||
.map_err(|_| "db_error")?;
|
||||
.unwrap_or(false);
|
||||
let has_required_user_right = column_exists(client.clone(), "chat", "room", "required_user_right_id")
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let gender_column = if has_gender_restriction {
|
||||
"r.gender_restriction_id"
|
||||
} else {
|
||||
"NULL::int"
|
||||
};
|
||||
let required_right_column = if has_required_user_right {
|
||||
"r.required_user_right_id"
|
||||
} else {
|
||||
"NULL::int"
|
||||
};
|
||||
let query = format!(
|
||||
"SELECT r.title, r.password_hash, r.is_public, r.owner_id, r.min_age, r.max_age, {gender_column} AS gender_restriction_id, {required_right_column} AS required_user_right_id
|
||||
FROM chat.room r
|
||||
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id"
|
||||
);
|
||||
let rows = client.query(&query, &[]).await.map_err(|_| "db_error")?;
|
||||
|
||||
let mut rooms = Vec::new();
|
||||
for row in rows {
|
||||
rooms.push(RoomMeta {
|
||||
name: row.get::<_, String>("title"),
|
||||
password: row.get::<_, Option<String>>("password_hash"),
|
||||
gender_restriction_id: row.get::<_, Option<i32>>("gender_restriction_id"),
|
||||
required_user_right_id: row.get::<_, Option<i32>>("required_user_right_id"),
|
||||
min_age: row.get::<_, Option<i32>>("min_age"),
|
||||
max_age: row.get::<_, Option<i32>>("max_age"),
|
||||
is_public: row.get::<_, bool>("is_public"),
|
||||
@@ -185,17 +203,25 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result<Vec<RoomMeta>, &
|
||||
}
|
||||
|
||||
async fn load_user_age(client: Arc<PgClient>, falukant_user_id: i32) -> Option<i32> {
|
||||
let row = client
|
||||
.query_opt(
|
||||
"SELECT up.value
|
||||
FROM community.user_param up
|
||||
JOIN \"type\".user_param tp ON up.param_type_id = tp.id
|
||||
WHERE up.user_id = $1 AND tp.description = 'birthdate'
|
||||
LIMIT 1",
|
||||
&[&falukant_user_id],
|
||||
)
|
||||
.await
|
||||
.ok()??;
|
||||
let mut row = None;
|
||||
for description in ["birthdate", "birthday", "geburtsdatum"] {
|
||||
row = client
|
||||
.query_opt(
|
||||
"SELECT up.value
|
||||
FROM community.user_param up
|
||||
JOIN \"type\".user_param tp ON up.param_type_id = tp.id
|
||||
WHERE up.user_id = $1 AND tp.description = $2
|
||||
LIMIT 1",
|
||||
&[&falukant_user_id, &description],
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
if row.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let row = row?;
|
||||
let encrypted_or_plain = row.get::<_, Option<String>>("value")?;
|
||||
let normalized = normalize_birthdate_value(&encrypted_or_plain)?;
|
||||
parse_age_from_yyyy_mm_dd(&normalized)
|
||||
@@ -276,3 +302,84 @@ fn decrypt_aes256_ecb_hex(encrypted_hex: &str) -> Option<String> {
|
||||
|
||||
String::from_utf8(out).ok()
|
||||
}
|
||||
|
||||
async fn load_user_gender(client: Arc<PgClient>, user_id: i32) -> Option<i32> {
|
||||
for query in [
|
||||
"SELECT gender_restriction_id AS gender_id FROM community.\"user\" WHERE id = $1 LIMIT 1",
|
||||
"SELECT gender_type_id AS gender_id FROM community.\"user\" WHERE id = $1 LIMIT 1",
|
||||
"SELECT gender_id AS gender_id FROM community.\"user\" WHERE id = $1 LIMIT 1",
|
||||
"SELECT sex_id AS gender_id FROM community.\"user\" WHERE id = $1 LIMIT 1",
|
||||
] {
|
||||
match client.query_opt(query, &[&user_id]).await {
|
||||
Ok(Some(row)) => {
|
||||
let value: Option<i32> = row.try_get("gender_id").ok();
|
||||
if value.unwrap_or(0) > 0 {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn column_exists(client: Arc<PgClient>, schema: &str, table: &str, column: &str) -> Result<bool, ()> {
|
||||
let row = client
|
||||
.query_one(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = $1
|
||||
AND table_name = $2
|
||||
AND column_name = $3
|
||||
) AS exists",
|
||||
&[&schema, &table, &column],
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ())?;
|
||||
Ok(row.get::<_, bool>("exists"))
|
||||
}
|
||||
|
||||
async fn load_chat_rights(client: Arc<PgClient>, chat_user_id: i32) -> Result<HashSet<String>, ()> {
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT r.tr
|
||||
FROM chat.user_rights ur
|
||||
JOIN chat.rights r ON ur.chat_right_id = r.id
|
||||
WHERE ur.chat_user_id = $1",
|
||||
&[&chat_user_id],
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ())?;
|
||||
let mut rights = HashSet::new();
|
||||
for row in rows {
|
||||
let right = row.get::<_, String>("tr");
|
||||
rights.insert(right.clone());
|
||||
rights.insert(right.to_lowercase());
|
||||
}
|
||||
Ok(rights)
|
||||
}
|
||||
|
||||
async fn load_community_rights(client: Arc<PgClient>, falukant_user_id: i32) -> Result<(HashSet<String>, HashSet<i32>), ()> {
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT tur.id AS right_type_id, tur.title
|
||||
FROM community.user_right ur
|
||||
JOIN \"type\".user_right tur ON ur.right_type_id = tur.id
|
||||
WHERE ur.user_id = $1",
|
||||
&[&falukant_user_id],
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ())?;
|
||||
let mut rights = HashSet::new();
|
||||
let mut right_ids = HashSet::new();
|
||||
for row in rows {
|
||||
let right_title = row.get::<_, String>("title");
|
||||
let right_id = row.get::<_, i32>("right_type_id");
|
||||
rights.insert(right_title.clone());
|
||||
rights.insert(right_title.to_lowercase());
|
||||
right_ids.insert(right_id);
|
||||
}
|
||||
Ok((rights, right_ids))
|
||||
}
|
||||
|
||||
20
src/main.rs
20
src/main.rs
@@ -232,15 +232,19 @@ async fn print_rooms_for_cli(
|
||||
if config.db_client.is_some() { "database" } else { "fallback" }
|
||||
);
|
||||
println!(
|
||||
"{:<30} {:<8} {:<8} {:<8} {:<10}",
|
||||
"name", "public", "min_age", "max_age", "password"
|
||||
"{:<24} {:<8} {:<8} {:<8} {:<8} {:<10} {:<8}",
|
||||
"name", "public", "gender", "min_age", "max_age", "password", "right_id"
|
||||
);
|
||||
println!("{}", "-".repeat(72));
|
||||
println!("{}", "-".repeat(92));
|
||||
for room in rooms {
|
||||
println!(
|
||||
"{:<30} {:<8} {:<8} {:<8} {:<10}",
|
||||
"{:<24} {:<8} {:<8} {:<8} {:<8} {:<10} {:<8}",
|
||||
room.name,
|
||||
if room.is_public { "yes" } else { "no" },
|
||||
room.gender_restriction_id
|
||||
.filter(|v| *v > 0)
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
room.min_age
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
@@ -252,6 +256,10 @@ async fn print_rooms_for_cli(
|
||||
} else {
|
||||
"set"
|
||||
},
|
||||
room.required_user_right_id
|
||||
.filter(|v| *v > 0)
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -283,8 +291,10 @@ where
|
||||
token: None,
|
||||
falukant_user_id: None,
|
||||
chat_user_id: None,
|
||||
gender_id: None,
|
||||
age: None,
|
||||
rights: HashSet::new(),
|
||||
right_type_ids: HashSet::new(),
|
||||
logged_in: false,
|
||||
tx: tx.clone(),
|
||||
},
|
||||
@@ -353,8 +363,10 @@ where
|
||||
token: None,
|
||||
falukant_user_id: None,
|
||||
chat_user_id: None,
|
||||
gender_id: None,
|
||||
age: None,
|
||||
rights: HashSet::new(),
|
||||
right_type_ids: HashSet::new(),
|
||||
logged_in: false,
|
||||
tx: tx.clone(),
|
||||
},
|
||||
|
||||
@@ -15,8 +15,10 @@ pub(crate) struct ClientConn {
|
||||
pub(crate) token: Option<String>,
|
||||
pub(crate) falukant_user_id: Option<i32>,
|
||||
pub(crate) chat_user_id: Option<i32>,
|
||||
pub(crate) gender_id: Option<i32>,
|
||||
pub(crate) age: Option<i32>,
|
||||
pub(crate) rights: HashSet<String>,
|
||||
pub(crate) right_type_ids: HashSet<i32>,
|
||||
pub(crate) logged_in: bool,
|
||||
pub(crate) tx: mpsc::UnboundedSender<String>,
|
||||
}
|
||||
@@ -43,8 +45,10 @@ pub(crate) struct UserProfile {
|
||||
pub(crate) color: Option<String>,
|
||||
pub(crate) falukant_user_id: Option<i32>,
|
||||
pub(crate) chat_user_id: Option<i32>,
|
||||
pub(crate) gender_id: Option<i32>,
|
||||
pub(crate) age: Option<i32>,
|
||||
pub(crate) rights: HashSet<String>,
|
||||
pub(crate) right_type_ids: HashSet<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -83,6 +87,8 @@ pub(crate) struct DiceGame {
|
||||
pub(crate) struct RoomMeta {
|
||||
pub(crate) name: String,
|
||||
pub(crate) password: Option<String>,
|
||||
pub(crate) gender_restriction_id: Option<i32>,
|
||||
pub(crate) required_user_right_id: Option<i32>,
|
||||
pub(crate) min_age: Option<i32>,
|
||||
pub(crate) max_age: Option<i32>,
|
||||
pub(crate) is_public: bool,
|
||||
|
||||
Reference in New Issue
Block a user