unilim-cas 1.0.1

A purrfect CAS authentication wrapper for Unilim.
Documentation
use scraper::{Html, Selector};
use std::collections::BTreeMap;
use std::sync::LazyLock;

static FORM: LazyLock<Selector> = LazyLock::new(|| Selector::parse("form").unwrap());
static HIDDEN_FIELDS: LazyLock<Selector> =
    LazyLock::new(|| Selector::parse("form input[type=hidden]").unwrap());
static SF_BUTTONS: LazyLock<Selector> =
    LazyLock::new(|| Selector::parse("form button[name=sf]").unwrap());
static TOKEN: LazyLock<Selector> = LazyLock::new(|| Selector::parse("input[name=token]").unwrap());
static CONFIRM: LazyLock<Selector> = LazyLock::new(|| Selector::parse("#confirm").unwrap());

pub struct Page(Html);

impl Page {
    pub fn parse(html: &str) -> Self {
        Self(Html::parse_document(html))
    }

    /// the `action` attribute of the first form.
    pub fn form_action(&self) -> Option<&str> {
        self.0
            .select(&FORM)
            .next()
            .and_then(|form| form.attr("action"))
    }

    /// every hidden form input as a name to value map, missing values becoming
    /// empty strings.
    pub fn hidden_fields(&self) -> BTreeMap<String, String> {
        self.0
            .select(&HIDDEN_FIELDS)
            .filter_map(|input| {
                let name = input.attr("name")?;
                Some((
                    name.to_owned(),
                    input.attr("value").unwrap_or_default().to_owned(),
                ))
            })
            .collect()
    }

    /// the csrf token of the login form.
    pub fn token(&self) -> Option<String> {
        self.value(&TOKEN)
    }

    /// the confirm value of the oauth2 consent page.
    pub fn confirm(&self) -> Option<String> {
        self.value(&CONFIRM)
    }

    /// which 2fa methods the form offers.
    pub fn twofa_methods(&self) -> TwoFaMethods {
        let mut methods = TwoFaMethods::default();
        for button in self.0.select(&SF_BUTTONS) {
            match button.attr("value") {
                Some("mail") => methods.email = true,
                Some("totp") => methods.totp = true,
                _ => {}
            }
        }
        methods
    }

    fn value(&self, selector: &Selector) -> Option<String> {
        self.0
            .select(selector)
            .next()
            .and_then(|element| element.attr("value"))
            .map(str::to_owned)
    }
}

#[derive(Debug, Default, Clone, Copy)]
pub struct TwoFaMethods {
    pub email: bool,
    pub totp: bool,
}