`http.rs` hand-rolled its own `block_on` + `map_http_err`, and `secrets.rs` /
`email.rs` / `users.rs` each carried a byte-identical `runtime_err` — five
`#[allow(clippy::unnecessary_box_returns)]` across four files for one pattern.
Move `runtime_err` to `bridge.rs` beside `block_on` (single home, single allow)
and have the three bridges import it. `http.rs` now calls the shared
`bridge::block_on("http", …)` (same "http:"-prefixed error, so messages are
unchanged) and its `err` validation helper delegates to `bridge::runtime_err`.
Deletes 4 duplicate fns and 3 of the 5 allows; no behavior change (pinned by
the executor-core SDK contract tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
618 lines
23 KiB
Rust
618 lines
23 KiB
Rust
//! `users::` Rhai bridge — data-plane user management (v1.1.8).
|
|
//!
|
|
//! ```rhai
|
|
//! // CRUD
|
|
//! let u = users::create(#{ email: "a@b", password: "hunter22a", display_name: "Alice" });
|
|
//! let u = users::get(id); // map or ()
|
|
//! let u = users::find_by_email("a@b"); // map or () — REQUIRES an
|
|
//! // authenticated principal
|
|
//! // (anti-enumeration); forbidden
|
|
//! // for anonymous public scripts.
|
|
//! let free = users::email_available("a@b"); // bool — anonymous-safe pre-check
|
|
//! // for self-serve registration.
|
|
//! // Leaks only "exists/doesn't" but
|
|
//! // is cheap + unthrottled: throttle
|
|
//! // the route if enumeration matters.
|
|
//! users::update(id, #{ display_name: "Alicia" });
|
|
//! let removed = users::delete(id); // bool
|
|
//! let page = users::list(#{ "$limit": 50, cursor: () });
|
|
//!
|
|
//! // Auth
|
|
//! let tok = users::login("a@b", "hunter22a"); // session-token string or ()
|
|
//! let u = users::verify(tok); // map or () (sliding-TTL bump)
|
|
//! users::logout(tok);
|
|
//!
|
|
//! // Email-tied (commit 5 / 6 / 7)
|
|
//! users::send_verification_email(id,
|
|
//! #{ link_base: "https://app/verify", subject: "Confirm", body_template: "..." });
|
|
//! let u = users::verify_email(token);
|
|
//! users::request_password_reset("a@b", #{...});
|
|
//! let u = users::complete_password_reset(token, "new_pw");
|
|
//! users::invite("a@b",
|
|
//! #{ link_base: "https://app/invite", subject: "Join", body_template: "...",
|
|
//! display_name: "Bob", roles: ["editor"] });
|
|
//! let session_tok = users::accept_invite(token, "pw", "Bob");
|
|
//!
|
|
//! // Roles (commit 9)
|
|
//! users::add_role(id, "admin");
|
|
//! let removed = users::remove_role(id, "admin"); // bool
|
|
//! let yes = users::has_role(id, "admin"); // bool
|
|
//! ```
|
|
//!
|
|
//! Collection-less surface (mirrors `email::` / `secrets::` rather
|
|
//! than `kv::`/`docs::`/`files::`'s handle pattern). `app_id` is
|
|
//! derived from `cx.app_id` in the service — it never appears on the
|
|
//! script-side signature, preserving cross-app isolation.
|
|
//!
|
|
//! Methods bound but not yet implemented in the underlying service
|
|
//! return a `users::not_implemented` runtime error. Subsequent v1.1.8
|
|
//! commits flesh them out without re-touching this file.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use super::bridge::{block_on, runtime_err};
|
|
use picloud_shared::{
|
|
AppUser, AppUserId, CreateUserInput, EmailTemplateOpts, GeneratedAccept, GeneratedSession,
|
|
InviteOpts, SdkCallCx, Services, UpdateUserInput, UsersListOpts, UsersListPage, UsersService,
|
|
};
|
|
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};
|
|
|
|
pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
|
|
let svc = services.users.clone();
|
|
let mut module = Module::new();
|
|
|
|
bind_create(&mut module, &svc, &cx);
|
|
bind_get(&mut module, &svc, &cx);
|
|
bind_find_by_email(&mut module, &svc, &cx);
|
|
bind_email_available(&mut module, &svc, &cx);
|
|
bind_update(&mut module, &svc, &cx);
|
|
bind_delete(&mut module, &svc, &cx);
|
|
bind_list(&mut module, &svc, &cx);
|
|
bind_login(&mut module, &svc, &cx);
|
|
bind_verify(&mut module, &svc, &cx);
|
|
bind_logout(&mut module, &svc, &cx);
|
|
bind_send_verification_email(&mut module, &svc, &cx);
|
|
bind_verify_email(&mut module, &svc, &cx);
|
|
bind_request_password_reset(&mut module, &svc, &cx);
|
|
bind_complete_password_reset(&mut module, &svc, &cx);
|
|
bind_invite(&mut module, &svc, &cx);
|
|
bind_accept_invite(&mut module, &svc, &cx);
|
|
bind_add_role(&mut module, &svc, &cx);
|
|
bind_remove_role(&mut module, &svc, &cx);
|
|
bind_has_role(&mut module, &svc, &cx);
|
|
|
|
engine.register_static_module("users", module.into());
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CRUD
|
|
// ----------------------------------------------------------------------------
|
|
|
|
fn bind_create(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"create",
|
|
move |opts: Map| -> Result<Map, Box<EvalAltResult>> {
|
|
let email = required_string(&opts, "email", "users::create")?;
|
|
let password = required_string(&opts, "password", "users::create")?;
|
|
let display_name = optional_string(&opts, "display_name");
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user = block_on("users", async move {
|
|
svc.create(
|
|
&cx,
|
|
CreateUserInput {
|
|
email,
|
|
password,
|
|
display_name,
|
|
},
|
|
)
|
|
.await
|
|
})?;
|
|
Ok(user_to_map(&user))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_get(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"get",
|
|
move |id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::get")?;
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user_opt = block_on("users", async move { svc.get(&cx, id).await })?;
|
|
Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_find_by_email(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"find_by_email",
|
|
move |email: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let email = email.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user_opt = block_on("users", async move { svc.find_by_email(&cx, &email).await })?;
|
|
Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
|
|
},
|
|
);
|
|
}
|
|
|
|
/// `users::email_available(email)` → `bool`. Anonymous-safe pre-check for
|
|
/// self-serve registration (unlike `find_by_email`, which forbids
|
|
/// anonymous callers). Lets a public register script branch on a free vs
|
|
/// taken email without relying on `create`'s uniqueness error/502.
|
|
fn bind_email_available(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"email_available",
|
|
move |email: &str| -> Result<bool, Box<EvalAltResult>> {
|
|
let email = email.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on(
|
|
"users",
|
|
async move { svc.email_available(&cx, &email).await },
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_update(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"update",
|
|
move |id: &str, patch: Map| -> Result<Map, Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::update")?;
|
|
// display_name as Some(None) means "clear"; absent means
|
|
// "leave alone". Differentiation: present-with-() vs absent.
|
|
let display_name = if patch.contains_key("display_name") {
|
|
Some(optional_string(&patch, "display_name"))
|
|
} else {
|
|
None
|
|
};
|
|
let patch = UpdateUserInput { display_name };
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user = block_on("users", async move { svc.update(&cx, id, patch).await })?;
|
|
Ok(user_to_map(&user))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_delete(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"delete",
|
|
move |id: &str| -> Result<bool, Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::delete")?;
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move { svc.delete(&cx, id).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_list(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"list",
|
|
move |opts: Map| -> Result<Map, Box<EvalAltResult>> {
|
|
let limit = match opts.get("$limit").or_else(|| opts.get("limit")) {
|
|
None => None,
|
|
Some(d) if d.is_unit() => None,
|
|
Some(d) => Some(
|
|
d.as_int()
|
|
.map_err(|_| runtime_err("users::list: '$limit' must be an integer"))?,
|
|
),
|
|
};
|
|
let cursor = match opts.get("cursor") {
|
|
None => None,
|
|
Some(d) if d.is_unit() => None,
|
|
Some(d) if d.is_string() => Some(d.clone().into_string().unwrap_or_default()),
|
|
Some(_) => return Err(runtime_err("users::list: cursor must be a string or ()")),
|
|
};
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let page: UsersListPage = block_on("users", async move {
|
|
svc.list(&cx, UsersListOpts { cursor, limit }).await
|
|
})?;
|
|
Ok(list_page_to_map(&page))
|
|
},
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Auth
|
|
// ----------------------------------------------------------------------------
|
|
|
|
fn bind_login(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"login",
|
|
move |email: &str, password: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let email = email.to_string();
|
|
let password = password.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let session_opt: Option<GeneratedSession> =
|
|
block_on(
|
|
"users",
|
|
async move { svc.login(&cx, &email, &password).await },
|
|
)?;
|
|
Ok(session_opt.map_or(Dynamic::UNIT, |s| Dynamic::from(s.token)))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_verify(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"verify",
|
|
move |token: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let token = token.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user_opt = block_on("users", async move { svc.verify(&cx, &token).await })?;
|
|
Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_logout(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"logout",
|
|
move |token: &str| -> Result<(), Box<EvalAltResult>> {
|
|
let token = token.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move { svc.logout(&cx, &token).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Email-tied (commit 5 / 6 / 7 fill the service impl behind these)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
fn bind_send_verification_email(
|
|
module: &mut Module,
|
|
svc: &Arc<dyn UsersService>,
|
|
cx: &Arc<SdkCallCx>,
|
|
) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"send_verification_email",
|
|
move |id: &str, opts: Map| -> Result<(), Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::send_verification_email")?;
|
|
let opts = parse_email_template(&opts, "users::send_verification_email")?;
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move {
|
|
svc.send_verification_email(&cx, id, opts).await
|
|
})
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_verify_email(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"verify_email",
|
|
move |token: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let token = token.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user_opt = block_on("users", async move { svc.verify_email(&cx, &token).await })?;
|
|
Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_request_password_reset(
|
|
module: &mut Module,
|
|
svc: &Arc<dyn UsersService>,
|
|
cx: &Arc<SdkCallCx>,
|
|
) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"request_password_reset",
|
|
move |email: &str, opts: Map| -> Result<(), Box<EvalAltResult>> {
|
|
let email = email.to_string();
|
|
let opts = parse_email_template(&opts, "users::request_password_reset")?;
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move {
|
|
svc.request_password_reset(&cx, &email, opts).await
|
|
})
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_complete_password_reset(
|
|
module: &mut Module,
|
|
svc: &Arc<dyn UsersService>,
|
|
cx: &Arc<SdkCallCx>,
|
|
) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"complete_password_reset",
|
|
move |token: &str, new_password: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let token = token.to_string();
|
|
let new_password = new_password.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let user_opt = block_on("users", async move {
|
|
svc.complete_password_reset(&cx, &token, &new_password)
|
|
.await
|
|
})?;
|
|
Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_invite(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"invite",
|
|
move |email: &str, opts: Map| -> Result<(), Box<EvalAltResult>> {
|
|
let email = email.to_string();
|
|
let opts = parse_invite_opts(&opts)?;
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move { svc.invite(&cx, &email, opts).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_accept_invite(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
// Two-arg overload: (token, password). The display_name overload is
|
|
// bound separately because Rhai resolves functions by arity.
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"accept_invite",
|
|
move |token: &str, password: &str| -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let token = token.to_string();
|
|
let password = password.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let accept_opt: Option<GeneratedAccept> = block_on("users", async move {
|
|
svc.accept_invite(&cx, &token, &password, None).await
|
|
})?;
|
|
Ok(accept_opt.map_or(Dynamic::UNIT, |a| Dynamic::from(a.session.token)))
|
|
},
|
|
);
|
|
}
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"accept_invite",
|
|
move |token: &str,
|
|
password: &str,
|
|
display_name: &str|
|
|
-> Result<Dynamic, Box<EvalAltResult>> {
|
|
let token = token.to_string();
|
|
let password = password.to_string();
|
|
let display_name = if display_name.trim().is_empty() {
|
|
None
|
|
} else {
|
|
Some(display_name.trim().to_string())
|
|
};
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
let accept_opt: Option<GeneratedAccept> = block_on("users", async move {
|
|
svc.accept_invite(&cx, &token, &password, display_name)
|
|
.await
|
|
})?;
|
|
Ok(accept_opt.map_or(Dynamic::UNIT, |a| Dynamic::from(a.session.token)))
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Roles (commit 9 fills the service impl)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
fn bind_add_role(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"add_role",
|
|
move |id: &str, role: &str| -> Result<(), Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::add_role")?;
|
|
let role = role.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move { svc.add_role(&cx, id, &role).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_remove_role(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"remove_role",
|
|
move |id: &str, role: &str| -> Result<bool, Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::remove_role")?;
|
|
let role = role.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on(
|
|
"users",
|
|
async move { svc.remove_role(&cx, id, &role).await },
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn bind_has_role(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"has_role",
|
|
move |id: &str, role: &str| -> Result<bool, Box<EvalAltResult>> {
|
|
let id = parse_user_id(id, "users::has_role")?;
|
|
let role = role.to_string();
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("users", async move { svc.has_role(&cx, id, &role).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Shape conversion helpers
|
|
// ----------------------------------------------------------------------------
|
|
|
|
fn user_to_map(user: &AppUser) -> Map {
|
|
let mut m = Map::new();
|
|
m.insert("id".into(), Dynamic::from(user.id.to_string()));
|
|
m.insert("email".into(), Dynamic::from(user.email.clone()));
|
|
m.insert(
|
|
"display_name".into(),
|
|
user.display_name
|
|
.clone()
|
|
.map_or(Dynamic::UNIT, Dynamic::from),
|
|
);
|
|
m.insert(
|
|
"email_verified_at".into(),
|
|
user.email_verified_at
|
|
.map_or(Dynamic::UNIT, |d| Dynamic::from(d.to_rfc3339())),
|
|
);
|
|
m.insert(
|
|
"last_login_at".into(),
|
|
user.last_login_at
|
|
.map_or(Dynamic::UNIT, |d| Dynamic::from(d.to_rfc3339())),
|
|
);
|
|
m.insert(
|
|
"created_at".into(),
|
|
Dynamic::from(user.created_at.to_rfc3339()),
|
|
);
|
|
m.insert(
|
|
"updated_at".into(),
|
|
Dynamic::from(user.updated_at.to_rfc3339()),
|
|
);
|
|
let roles: Array = user.roles.iter().cloned().map(Dynamic::from).collect();
|
|
m.insert("roles".into(), roles.into());
|
|
m
|
|
}
|
|
|
|
fn list_page_to_map(page: &UsersListPage) -> Map {
|
|
let mut m = Map::new();
|
|
let items: Array = page
|
|
.items
|
|
.iter()
|
|
.map(|u| Dynamic::from(user_to_map(u)))
|
|
.collect();
|
|
m.insert("users".into(), items.into());
|
|
m.insert(
|
|
"next_cursor".into(),
|
|
page.next_cursor
|
|
.as_ref()
|
|
.map_or(Dynamic::UNIT, |s| Dynamic::from(s.clone())),
|
|
);
|
|
m
|
|
}
|
|
|
|
fn parse_user_id(s: &str, ctx: &str) -> Result<AppUserId, Box<EvalAltResult>> {
|
|
uuid::Uuid::parse_str(s)
|
|
.map(AppUserId::from)
|
|
.map_err(|e| runtime_err(&format!("{ctx}: id must be a UUID: {e}")))
|
|
}
|
|
|
|
fn parse_email_template(opts: &Map, ctx: &str) -> Result<EmailTemplateOpts, Box<EvalAltResult>> {
|
|
Ok(EmailTemplateOpts {
|
|
link_base: required_string(opts, "link_base", ctx)?,
|
|
from: required_string(opts, "from", ctx)?,
|
|
subject: required_string(opts, "subject", ctx)?,
|
|
body_template: required_string(opts, "body_template", ctx)?,
|
|
})
|
|
}
|
|
|
|
fn parse_invite_opts(opts: &Map) -> Result<InviteOpts, Box<EvalAltResult>> {
|
|
// The template fields are optional only when invite is configured
|
|
// to ship without an email; the service decides which is the
|
|
// hard error. Here we forward whatever the script gave us.
|
|
let template = if opts.contains_key("link_base")
|
|
|| opts.contains_key("subject")
|
|
|| opts.contains_key("body_template")
|
|
{
|
|
Some(parse_email_template(opts, "users::invite")?)
|
|
} else {
|
|
None
|
|
};
|
|
let display_name = optional_string(opts, "display_name");
|
|
let roles = match opts.get("roles") {
|
|
None => Vec::new(),
|
|
Some(d) if d.is_unit() => Vec::new(),
|
|
Some(d) => {
|
|
if let Some(arr) = d.clone().try_cast::<Array>() {
|
|
let mut out = Vec::with_capacity(arr.len());
|
|
for el in arr {
|
|
if !el.is_string() {
|
|
return Err(runtime_err(
|
|
"users::invite: 'roles' must be an array of strings",
|
|
));
|
|
}
|
|
out.push(el.into_string().unwrap_or_default());
|
|
}
|
|
out
|
|
} else {
|
|
return Err(runtime_err(
|
|
"users::invite: 'roles' must be an array of strings",
|
|
));
|
|
}
|
|
}
|
|
};
|
|
Ok(InviteOpts {
|
|
template,
|
|
display_name,
|
|
roles,
|
|
})
|
|
}
|
|
|
|
fn required_string(opts: &Map, key: &str, ctx: &str) -> Result<String, Box<EvalAltResult>> {
|
|
match opts.get(key) {
|
|
Some(d) if d.is_string() => Ok(d.clone().into_string().unwrap_or_default()),
|
|
_ => Err(runtime_err(&format!(
|
|
"{ctx}: '{key}' must be a string and is required"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn optional_string(opts: &Map, key: &str) -> Option<String> {
|
|
match opts.get(key) {
|
|
None => None,
|
|
Some(d) if d.is_unit() => None,
|
|
Some(d) if d.is_string() => Some(d.clone().into_string().unwrap_or_default()),
|
|
Some(d) => Some(d.to_string()),
|
|
}
|
|
}
|