use std::future::Future;
use std::pin::Pin;
use crate::core::SqlValue;
pub type DeriveFuture<'a> = Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
pub type DeriveFn = for<'a> fn(
&'a mut Vec<(&'static str, SqlValue)>,
Option<&'a serde_json::Value>,
) -> DeriveFuture<'a>;
pub struct AdminDerivedField {
pub table: &'static str,
pub derive: DeriveFn,
}
inventory::collect!(AdminDerivedField);
pub(crate) async fn apply(
table: &str,
values: &mut Vec<(&'static str, SqlValue)>,
before: Option<&serde_json::Value>,
) -> Result<(), String> {
for entry in inventory::iter::<AdminDerivedField> {
if entry.table == table {
(entry.derive)(values, before).await?;
}
}
Ok(())
}
#[cfg_attr(not(feature = "sso"), allow(dead_code))]
pub(crate) fn text(
values: &[(&'static str, SqlValue)],
before: Option<&serde_json::Value>,
column: &str,
) -> Option<String> {
values
.iter()
.find(|(c, _)| *c == column)
.and_then(|(_, v)| match v {
SqlValue::String(s) => Some(s.clone()),
_ => None,
})
.or_else(|| before?.get(column)?.as_str().map(str::to_owned))
}
pub(crate) fn revoke_on_deactivate<'a>(
values: &'a mut Vec<(&'static str, SqlValue)>,
before: Option<&'a serde_json::Value>,
) -> DeriveFuture<'a> {
use crate::session::SESSIONS_REVOKED_AT;
Box::pin(async move {
let was_active = before
.and_then(|r| r.get("active"))
.is_some_and(|v| v.as_bool() == Some(true) || v.as_i64().is_some_and(|n| n != 0));
let deactivates = values
.iter()
.any(|(c, v)| *c == "active" && matches!(v, SqlValue::Bool(false)));
if !(was_active && deactivates) {
return Ok(());
}
let prev = before
.and_then(|r| r.get(SESSIONS_REVOKED_AT)?.as_str())
.and_then(|s| s.parse::<chrono::DateTime<chrono::Utc>>().ok());
let cutoff = crate::session::logout_cutoff_at(prev, 0);
values.retain(|(c, _)| *c != SESSIONS_REVOKED_AT);
values.push((SESSIONS_REVOKED_AT, SqlValue::DateTime(cutoff)));
Ok::<(), String>(())
})
}
pub(crate) fn take_secret(
values: &mut Vec<(&'static str, SqlValue)>,
column: &str,
) -> Option<String> {
let i = values.iter().position(|(c, _)| *c == column)?;
match values.remove(i).1 {
SqlValue::String(s) => Some(s),
_ => None,
}
}
pub(crate) fn take_new_password(
values: &mut Vec<(&'static str, SqlValue)>,
before: Option<&serde_json::Value>,
column: &str,
) -> Result<Option<String>, String> {
let Some(plain) = take_secret(values, column) else {
return Ok(None);
};
if before.and_then(|r| r.get(column)?.as_str()) == Some(plain.as_str()) {
return Ok(None);
}
crate::password_validators::check_builtin_form_password(&plain).map_err(|e| e.message)?;
Ok(Some(plain))
}