1
//! User-domain natives. Wraps `server::command::VerifyUserPassword` only;
2
//! account-creation/registration is reserved for the auth-flow path, not the
3
//! eval channel.
4
//!
5
//! `verify-user-password` is exposed verbatim from the server command —
6
//! takes (email, password), returns the matching UUID on success or nil
7
//! on bad creds. The session is already authenticated by the time forms
8
//! reach the eval channel, so this native exists for "confirm current
9
//! identity before sensitive action" workflows the emacs client may
10
//! drive (e.g. re-prompting on session-reauth UX).
11

            
12
use scripting::runtime::{alloc_string_ref, read_string_arg};
13
use server::command::CmdResult;
14
use server::command::user::VerifyUserPassword;
15
use wasmtime::{ArrayRef, Caller, Linker, Rooted};
16

            
17
use crate::session::SessionData;
18

            
19
pub const REGISTERED_COMMANDS: &[&str] = &["verify-user-password"];
20

            
21
2559
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
22
2559
    linker.func_wrap_async(
23
2559
        "nomi",
24
2559
        "user_verify_user_password",
25
        |mut caller: Caller<'_, SessionData>,
26
         (email_arg, password_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
27
         -> Box<
28
            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
29
18
        > {
30
18
            Box::new(async move {
31
18
                let email = read_string_arg(&mut caller, email_arg)?;
32
18
                let password = read_string_arg(&mut caller, password_arg)?;
33
18
                match run_verify_user_password(email, password).await? {
34
                    Some(id) => Ok(Some(alloc_string_ref(&mut caller, id.as_bytes())?)),
35
18
                    None => Ok(None),
36
                }
37
18
            })
38
18
        },
39
    )?;
40
2559
    Ok(())
41
2559
}
42

            
43
/// On success returns `Some(<uuid-string>)`; bad credentials surface as
44
/// `Ok(None)` (null `ref null $i8_array` on the wire). Validation/runtime
45
/// failures trap via `wasmtime::Error` and the rpc envelope renders them
46
/// as `:error` records.
47
21
async fn run_verify_user_password(
48
21
    email_arg: Option<String>,
49
21
    password_arg: Option<String>,
50
21
) -> wasmtime::Result<Option<String>> {
51
21
    let email = email_arg
52
21
        .filter(|s| !s.is_empty())
53
21
        .ok_or_else(|| wasmtime::Error::msg("verify-user-password: missing or empty :email arg"))?;
54
19
    let password = password_arg
55
19
        .ok_or_else(|| wasmtime::Error::msg("verify-user-password: missing :password arg"))?;
56
18
    match VerifyUserPassword::new()
57
18
        .email(email)
58
18
        .password(password)
59
18
        .run()
60
18
        .await
61
    {
62
        Ok(Some(CmdResult::Uuid(id))) => Ok(Some(id.to_string())),
63
18
        Ok(Some(CmdResult::Bool(false))) | Ok(None) => Ok(None),
64
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
65
            "verify-user-password: unexpected variant {other:?}"
66
        ))),
67
        Err(err) => Err(wasmtime::Error::msg(format!("verify-user-password: {err}"))),
68
    }
69
21
}
70

            
71
#[cfg(test)]
72
mod tests {
73
    use super::*;
74

            
75
    #[tokio::test]
76
1
    async fn run_verify_user_password_missing_email_emits_error() {
77
1
        let err = run_verify_user_password(None, Some("pw".into()))
78
1
            .await
79
1
            .unwrap_err();
80
1
        assert!(err.to_string().contains(":email"), "got: {err}");
81
1
    }
82

            
83
    #[tokio::test]
84
1
    async fn run_verify_user_password_empty_email_emits_error() {
85
1
        let err = run_verify_user_password(Some(String::new()), Some("pw".into()))
86
1
            .await
87
1
            .unwrap_err();
88
1
        assert!(err.to_string().contains(":email"), "got: {err}");
89
1
    }
90

            
91
    #[tokio::test]
92
1
    async fn run_verify_user_password_missing_password_emits_error() {
93
1
        let err = run_verify_user_password(Some("u@example.com".into()), None)
94
1
            .await
95
1
            .unwrap_err();
96
1
        assert!(err.to_string().contains(":password"), "got: {err}");
97
1
    }
98
}