Skip to main content

alibi_plugins/admin/
types.rs

1use chrono::{DateTime, SecondsFormat, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Role input accepted by TypeScript admin routes.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(untagged)]
8pub enum RoleInput {
9    /// One literal role name.
10    One(String),
11    /// Literal names stored in order as a comma-separated role.
12    Many(Vec<String>),
13}
14
15impl RoleInput {
16    pub(crate) fn joined(&self) -> String {
17        match self {
18            Self::One(role) => role.clone(),
19            Self::Many(roles) => roles.join(","),
20        }
21    }
22
23    pub(crate) fn roles(&self) -> Vec<&str> {
24        match self {
25            Self::One(role) => vec![role.as_str()],
26            Self::Many(roles) => roles.iter().map(String::as_str).collect(),
27        }
28    }
29}
30
31/// Input for the admin role assignment server operation.
32#[derive(Debug, Serialize, Deserialize)]
33pub struct SetRoleRequest {
34    #[serde(rename = "userId")]
35    /// The target user identifier.
36    pub user_id: String,
37    /// Literal role names to assign.
38    pub role: RoleInput,
39}
40
41#[derive(Debug, Deserialize)]
42pub(crate) struct GetUserQuery {
43    pub id: String,
44}
45
46/// Input for trusted or authenticated admin user provisioning.
47#[derive(Debug, Serialize, Deserialize)]
48pub struct CreateUserRequest {
49    /// The new user's email, normalized before storage.
50    pub email: String,
51    /// Optional credential password; empty strings create no credential.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub password: Option<String>,
54    /// The new user's display name.
55    pub name: String,
56    /// Optional literal role selector, taking precedence over data.role.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub role: Option<RoleInput>,
59    /// Optional application fields, subject to the installed permission policy.
60    #[serde(
61        default,
62        skip_serializing_if = "Option::is_none",
63        deserialize_with = "alibi_core::utils::json::deserialize_optional_map"
64    )]
65    pub data: Option<serde_json::Map<String, serde_json::Value>>,
66}
67impl CreateUserRequest {
68    /// Provision a user without a credential or an explicit role selector.
69    #[must_use]
70    pub fn new(email: impl Into<String>, name: impl Into<String>) -> Self {
71        Self {
72            email: email.into(),
73            name: name.into(),
74            password: None,
75            role: None,
76            data: None,
77        }
78    }
79}
80
81/// Target and application fields for an authenticated admin update.
82#[derive(Debug, Serialize, Deserialize)]
83pub struct AdminUpdateUserRequest {
84    /// The user to update.
85    #[serde(rename = "userId")]
86    pub user_id: String,
87    /// Fields admitted by the installed admin and user policies.
88    #[serde(deserialize_with = "alibi_core::utils::json::deserialize_map")]
89    pub data: serde_json::Map<String, serde_json::Value>,
90}
91impl AdminUpdateUserRequest {
92    /// Update the specified user with the supplied application fields.
93    #[must_use]
94    pub fn new(
95        user_id: impl Into<String>,
96        data: serde_json::Map<String, serde_json::Value>,
97    ) -> Self {
98        Self {
99            user_id: user_id.into(),
100            data,
101        }
102    }
103}
104
105#[derive(Debug, Deserialize)]
106pub(crate) struct UserIdRequest {
107    #[serde(rename = "userId")]
108    pub user_id: String,
109}
110
111/// Input for authoritative admin user banning.
112#[derive(Debug, Serialize, Deserialize)]
113pub struct BanUserRequest {
114    #[serde(rename = "userId")]
115    /// The stored user to ban.
116    pub user_id: String,
117    #[serde(rename = "banReason", skip_serializing_if = "Option::is_none")]
118    /// Optional reason, falling back to the configured reason.
119    pub ban_reason: Option<String>,
120    #[serde(
121        rename = "banExpiresIn",
122        skip_serializing_if = "Option::is_none",
123        default,
124        deserialize_with = "finite_optional_duration"
125    )]
126    /// Optional duration in seconds; zero falls back to the configured duration.
127    pub ban_expires_in: Option<f64>,
128}
129impl BanUserRequest {
130    /// Ban the specified user with the installed default reason and duration.
131    #[must_use]
132    pub fn new(user_id: impl Into<String>) -> Self {
133        Self {
134            user_id: user_id.into(),
135            ban_reason: None,
136            ban_expires_in: None,
137        }
138    }
139}
140
141#[derive(Debug, Deserialize)]
142pub(crate) struct RevokeSessionRequest {
143    #[serde(rename = "sessionToken")]
144    pub session_token: String,
145}
146
147/// Input for an authoritative administrative password update.
148#[derive(Debug, Serialize, Deserialize)]
149pub struct SetUserPasswordRequest {
150    /// The stored user whose credential should be replaced or created.
151    #[serde(rename = "userId")]
152    pub user_id: String,
153    /// The new password, subject to the installed password policy.
154    #[serde(rename = "newPassword")]
155    pub new_password: String,
156}
157impl SetUserPasswordRequest {
158    /// Set a password for the specified user without revoking existing sessions.
159    #[must_use]
160    pub fn new(user_id: impl Into<String>, new_password: impl Into<String>) -> Self {
161        Self {
162            user_id: user_id.into(),
163            new_password: new_password.into(),
164        }
165    }
166}
167
168/// Input for trusted or session-based admin permission evaluation.
169#[derive(Debug, Serialize, Deserialize)]
170pub struct HasPermissionRequest {
171    /// User selector for a trusted call; an authenticated session takes precedence.
172    #[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
173    pub user_id: Option<String>,
174    /// Literal role selector for a trusted call.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub role: Option<String>,
177    /// Singular schema alternative; the reference operation requires the plural field.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub permission: Option<HashMap<String, Vec<String>>>,
180    /// Requested actions grouped by resource.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub permissions: Option<HashMap<String, Vec<String>>>,
183}
184
185impl HasPermissionRequest {
186    /// Check the supplied resource actions, optionally selecting a trusted user or role.
187    #[must_use]
188    pub fn new(permissions: HashMap<String, Vec<String>>) -> Self {
189        Self {
190            permissions: Some(permissions),
191            user_id: None,
192            role: None,
193            permission: None,
194        }
195    }
196
197    pub(crate) fn requested_permissions(&self) -> Option<&HashMap<String, Vec<String>>> {
198        self.permissions.as_ref().or(self.permission.as_ref())
199    }
200}
201
202#[derive(Debug, Serialize)]
203pub(crate) struct PhysicalAdminUserView {
204    pub id: String,
205    pub name: Option<String>,
206    pub email: Option<String>,
207    #[serde(rename = "emailVerified")]
208    pub email_verified: bool,
209    pub image: Option<String>,
210    #[serde(rename = "createdAt")]
211    #[serde(serialize_with = "alibi_core::utils::datetime::serialize")]
212    pub created_at: DateTime<Utc>,
213    #[serde(rename = "updatedAt")]
214    #[serde(serialize_with = "alibi_core::utils::datetime::serialize")]
215    pub updated_at: DateTime<Utc>,
216    pub username: Option<String>,
217    #[serde(rename = "displayUsername")]
218    pub display_username: Option<String>,
219    #[serde(rename = "twoFactorEnabled")]
220    pub two_factor_enabled: bool,
221    pub role: Option<String>,
222    pub banned: bool,
223    #[serde(rename = "banReason")]
224    pub ban_reason: Option<String>,
225    #[serde(rename = "banExpires")]
226    pub ban_expires: Option<String>,
227}
228
229impl<T: alibi_core::entity::AuthUser> From<&T> for PhysicalAdminUserView {
230    fn from(user: &T) -> Self {
231        Self {
232            id: user.id().into_owned(),
233            name: user.name().map(str::to_owned),
234            email: user.email().map(str::to_owned),
235            email_verified: user.email_verified(),
236            image: user.image().map(str::to_owned),
237            created_at: user.created_at(),
238            updated_at: user.updated_at(),
239            username: user.username().map(str::to_owned),
240            display_username: user.display_username().map(str::to_owned),
241            two_factor_enabled: user.two_factor_enabled(),
242            role: user.role().map(str::to_owned),
243            banned: user.banned(),
244            ban_reason: user.ban_reason().map(str::to_owned),
245            ban_expires: user
246                .ban_expires()
247                .map(|value| value.to_rfc3339_opts(SecondsFormat::Millis, true)),
248        }
249    }
250}
251
252/// Public admin user projection, including returned application fields.
253#[derive(Debug, Serialize, Deserialize)]
254#[serde(transparent)]
255pub struct AdminUserView(serde_json::Map<String, serde_json::Value>);
256
257impl AdminUserView {
258    /// Access the safe public user fields.
259    #[must_use]
260    pub const fn values(&self) -> &serde_json::Map<String, serde_json::Value> {
261        &self.0
262    }
263
264    pub(crate) fn from_output<S: alibi_core::AuthSchema>(
265        ctx: &alibi_core::AuthContext<S>,
266        user: &alibi_core::AdapterRecord<S::User>,
267    ) -> alibi_core::AuthResult<Self> {
268        let serde_json::Value::Object(mut output) =
269            serde_json::to_value(PhysicalAdminUserView::from(user))?
270        else {
271            return Err(alibi_core::AuthError::internal(
272                "Admin user output must be an object",
273            ));
274        };
275        let registered = ctx.extensions.get::<alibi_core::field_policy::UserFields>();
276        let fields = registered
277            .as_ref()
278            .map_or(&ctx.config.user.additional_fields, |fields| &fields.0.0);
279        for (name, field) in fields {
280            _ = output.remove(name);
281            if field.returned
282                && let Some(value) = user.raw_snapshot().values().get(name)
283            {
284                _ = output.insert(name.clone(), value.clone());
285            }
286        }
287        let view = ctx.user_view(user);
288        if let Some(value) = view.is_anonymous {
289            _ = output.insert("isAnonymous".into(), serde_json::Value::Bool(value));
290        }
291        output.extend(view.extension_fields);
292        Ok(Self(output))
293    }
294}
295
296/// Public admin operation result containing a safe user projection.
297#[derive(Debug, Serialize, Deserialize)]
298pub struct UserResponse<U> {
299    /// The requested user after the operation.
300    pub user: U,
301}
302
303/// Session and safe user published by an administrative session operation.
304#[derive(Debug, Serialize, Deserialize)]
305pub struct SessionUserResponse<S, U> {
306    /// The persisted session owned by the operation.
307    pub session: S,
308    /// The user whose session is active after the operation.
309    pub user: U,
310}
311
312/// Public admin listing with safe user projections and parsed pagination.
313#[derive(Debug, Serialize, Deserialize)]
314pub struct ListUsersResponse<U> {
315    /// The selected page of users.
316    pub users: Vec<U>,
317    /// Total matching users before pagination.
318    pub total: usize,
319    /// The effective nonzero limit supplied by the caller.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub limit: Option<f64>,
322    /// The effective nonzero offset supplied by the caller.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub offset: Option<f64>,
325}
326
327/// Sessions selected for an administrative target lookup.
328#[derive(Debug, Serialize, Deserialize)]
329pub struct ListSessionsResponse<S> {
330    /// Persisted session projections for the requested user.
331    pub sessions: Vec<S>,
332}
333
334/// Result of a successful administrative mutation.
335#[derive(Debug, Serialize, Deserialize)]
336pub struct SuccessResponse {
337    /// Whether the requested operation completed.
338    pub success: bool,
339}
340
341/// Result of a successfully evaluated permission request.
342#[derive(Debug, Serialize, Deserialize)]
343pub struct PermissionResponse {
344    /// The reference successful result carries a null error.
345    pub error: Option<String>,
346    /// Whether one configured role authorizes all requested resource actions.
347    pub success: bool,
348}
349
350/// Query parameters for `list_users`.
351#[derive(Debug, Default, Deserialize)]
352pub(crate) struct ListUsersQueryParams {
353    pub source_limit: Option<f64>,
354    pub source_offset: Option<f64>,
355    pub limit: Option<usize>,
356    pub offset: Option<usize>,
357    #[serde(rename = "searchField")]
358    pub search_field: Option<String>,
359    #[serde(rename = "searchValue")]
360    pub search_value: Option<String>,
361    #[serde(rename = "searchOperator")]
362    pub search_operator: Option<String>,
363    #[serde(rename = "sortBy")]
364    pub sort_by: Option<String>,
365    #[serde(rename = "sortDirection")]
366    pub sort_direction: Option<String>,
367    #[serde(rename = "filterField")]
368    pub filter_field: Option<String>,
369    #[serde(rename = "filterValue")]
370    pub filter_value: Option<alibi_core::UserFilterValue>,
371    #[serde(rename = "filterOperator")]
372    pub filter_operator: Option<String>,
373}
374
375fn finite_optional_duration<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
376where
377    D: serde::Deserializer<'de>,
378{
379    let duration = Option::<f64>::deserialize(deserializer)?;
380    if duration.is_some_and(|value| !value.is_finite()) {
381        return Err(serde::de::Error::custom("banExpiresIn must be finite"));
382    }
383    Ok(duration)
384}
385
386/// Logical query for admin listing; scalar and array filters retain their JSON types.
387#[derive(Debug, Default, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct AdminListUsersQuery {
390    /// A numeric or string limit, interpreted with JavaScript Number semantics.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub limit: Option<serde_json::Value>,
393    /// A numeric or string offset, interpreted with JavaScript Number semantics.
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub offset: Option<serde_json::Value>,
396    /// Text to search for.
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub search_value: Option<String>,
399    /// Search field: email or name.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub search_field: Option<String>,
402    /// Search operator: contains, `starts_with` or `ends_with`.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub search_operator: Option<String>,
405    /// Field to sort by.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub sort_by: Option<String>,
408    /// Ascending or descending order.
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub sort_direction: Option<String>,
411    /// Field to filter, defaulting to email.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub filter_field: Option<String>,
414    /// A string, number, boolean, or homogeneous string/number array.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub filter_value: Option<serde_json::Value>,
417    /// The comparison operator for the filter.
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub filter_operator: Option<String>,
420}