Implement user gender handling and enhance gender-based access validation

Added functionality to load user gender from user parameters, improving the accuracy of gender-based access control. Introduced a new `normalize_gender_value` function to standardize gender inputs and updated the `room_access_allowed` function to utilize configured gender IDs for logging and validation. Enhanced debug logging to include configured gender IDs for better troubleshooting. This update ensures more robust handling of user gender in the application.
This commit is contained in:
Torsten Schulz (local)
2026-03-04 23:54:49 +01:00
parent a09926f48a
commit 37767e59a0
2 changed files with 120 additions and 6 deletions

View File

@@ -1192,14 +1192,17 @@ async fn room_access_allowed(
if let Some(required_gender) = room.gender_restriction_id { if let Some(required_gender) = room.gender_restriction_id {
if required_gender > 0 && user_gender_id != Some(required_gender) { if required_gender > 0 && user_gender_id != Some(required_gender) {
if room_debug_enabled() { if room_debug_enabled() {
let (male_id, female_id) = configured_gender_ids();
eprintln!( eprintln!(
"[yourchat2][room-debug][access] denied=room_gender_restricted room='{}' required_gender_id={} required_gender='{}' user_gender_id={:?} user_gender='{}' user_falukant_id={:?}", "[yourchat2][room-debug][access] denied=room_gender_restricted room='{}' required_gender_id={} required_gender='{}' user_gender_id={:?} user_gender='{}' user_falukant_id={:?} configured_gender_ids={{male:{},female:{}}}",
room.name, room.name,
required_gender, required_gender,
gender_label_from_id(Some(required_gender)), gender_label_from_id(Some(required_gender)),
user_gender_id, user_gender_id,
gender_label_from_id(user_gender_id), gender_label_from_id(user_gender_id),
falukant_user_id falukant_user_id,
male_id,
female_id
); );
} }
return Err("room_gender_restricted"); return Err("room_gender_restricted");
@@ -1639,9 +1642,10 @@ fn value_as_string(value: &Value) -> Option<String> {
} }
fn parse_gender_mf(value: &str) -> Option<Option<i32>> { fn parse_gender_mf(value: &str) -> Option<Option<i32>> {
let (male_id, female_id) = configured_gender_ids();
match value.trim().to_lowercase().as_str() { match value.trim().to_lowercase().as_str() {
"m" | "male" => Some(Some(1)), "m" | "male" => Some(Some(male_id)),
"f" | "female" => Some(Some(2)), "f" | "female" => Some(Some(female_id)),
"any" | "none" | "-" => Some(None), "any" | "none" | "-" => Some(None),
_ => None, _ => None,
} }
@@ -1662,14 +1666,29 @@ fn room_debug_enabled() -> bool {
} }
fn gender_label_from_id(gender_id: Option<i32>) -> &'static str { fn gender_label_from_id(gender_id: Option<i32>) -> &'static str {
let (male_id, female_id) = configured_gender_ids();
match gender_id { match gender_id {
Some(1) => "m", Some(v) if v == male_id => "m",
Some(2) => "f", Some(v) if v == female_id => "f",
Some(_) => "unknown", Some(_) => "unknown",
None => "none", None => "none",
} }
} }
fn configured_gender_ids() -> (i32, i32) {
let male_id = env::var("CHAT_GENDER_MALE_ID")
.ok()
.and_then(|v| v.trim().parse::<i32>().ok())
.filter(|v| *v > 0)
.unwrap_or(1);
let female_id = env::var("CHAT_GENDER_FEMALE_ID")
.ok()
.and_then(|v| v.trim().parse::<i32>().ok())
.filter(|v| *v > 0)
.unwrap_or(2);
(male_id, female_id)
}
fn command_from_chat_line(message: &str, token: Option<String>) -> Option<Command> { fn command_from_chat_line(message: &str, token: Option<String>) -> Option<Command> {
let trimmed = message.trim(); let trimmed = message.trim();
if !trimmed.starts_with('/') { if !trimmed.starts_with('/') {

View File

@@ -340,6 +340,10 @@ fn decrypt_aes256_ecb_hex(encrypted_hex: &str) -> Option<String> {
} }
async fn load_user_gender(client: Arc<PgClient>, user_id: i32) -> Option<i32> { async fn load_user_gender(client: Arc<PgClient>, user_id: i32) -> Option<i32> {
if let Some(mapped) = load_user_gender_from_user_param(client.clone(), user_id).await {
return Some(mapped);
}
for query in [ for query in [
"SELECT gender_restriction_id AS gender_id FROM community.\"user\" WHERE id = $1 LIMIT 1", "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_type_id AS gender_id FROM community.\"user\" WHERE id = $1 LIMIT 1",
@@ -357,6 +361,67 @@ async fn load_user_gender(client: Arc<PgClient>, user_id: i32) -> Option<i32> {
Err(_) => {} Err(_) => {}
} }
} }
for description in ["gender", "sex", "geschlecht"] {
if let Ok(Some(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",
&[&user_id, &description],
)
.await
{
if let Some(value) = row.get::<_, Option<String>>("value") {
if let Some(parsed) = normalize_gender_value(&value) {
return Some(parsed);
}
}
}
}
None
}
async fn load_user_gender_from_user_param(client: Arc<PgClient>, user_id: i32) -> Option<i32> {
let row = client
.query_opt(
"SELECT
up.value AS raw_value,
tpv_id.value AS mapped_by_id_value,
tpv_order.value AS mapped_by_order_value
FROM community.user_param up
JOIN \"type\".user_param tp
ON up.param_type_id = tp.id
LEFT JOIN \"type\".user_param_value tpv_id
ON tpv_id.user_param_type_id = tp.id
AND tpv_id.id::text = up.value
LEFT JOIN \"type\".user_param_value tpv_order
ON tpv_order.user_param_type_id = tp.id
AND tpv_order.order_id::text = up.value
WHERE up.user_id = $1
AND tp.description = 'gender'
LIMIT 1",
&[&user_id],
)
.await
.ok()??;
let raw_value = row.get::<_, Option<String>>("raw_value");
let mapped_by_id_value = row.get::<_, Option<String>>("mapped_by_id_value");
let mapped_by_order_value = row.get::<_, Option<String>>("mapped_by_order_value");
for candidate in [
mapped_by_id_value.as_deref(),
mapped_by_order_value.as_deref(),
raw_value.as_deref(),
] {
if let Some(value) = candidate {
if let Some(parsed) = normalize_gender_value(value) {
return Some(parsed);
}
}
}
None None
} }
@@ -499,3 +564,33 @@ async fn resolve_owner_falukant_id(client: Arc<PgClient>, room_owner_id: i32) ->
.ok()??; .ok()??;
row.get::<_, Option<i32>>("falukant_user_id") row.get::<_, Option<i32>>("falukant_user_id")
} }
fn normalize_gender_value(raw: &str) -> Option<i32> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
if let Ok(id) = trimmed.parse::<i32>() {
return (id > 0).then_some(id);
}
let (male_id, female_id) = configured_gender_ids();
match trimmed.to_lowercase().as_str() {
"m" | "male" | "mann" | "männlich" | "maennlich" | "transmale" => Some(male_id),
"f" | "female" | "frau" | "weiblich" | "transfemale" => Some(female_id),
_ => None,
}
}
fn configured_gender_ids() -> (i32, i32) {
let male_id = env::var("CHAT_GENDER_MALE_ID")
.ok()
.and_then(|v| v.trim().parse::<i32>().ok())
.filter(|v| *v > 0)
.unwrap_or(1);
let female_id = env::var("CHAT_GENDER_FEMALE_ID")
.ok()
.and_then(|v| v.trim().parse::<i32>().ok())
.filter(|v| *v > 0)
.unwrap_or(2);
(male_id, female_id)
}