Skip to main content

alibi_api/plugins/
account_management.rs

1use super::StatusResponse;
2use alibi_core::entity::{AuthAccount, AuthUser};
3use alibi_core::{AuthContext, AuthError, AuthResult};
4use alibi_core::{AuthRequest, AuthResponse};
5use serde::Deserialize;
6use validator::Validate;
7
8/// Account management plugin for listing and unlinking user accounts.
9pub struct AccountManagementPlugin {
10    config: AccountManagementConfig,
11}
12
13#[derive(Debug, Clone, alibi_core::PluginConfig)]
14#[plugin(name = "AccountManagementPlugin")]
15pub struct AccountManagementConfig {
16    #[config(default = true)]
17    pub require_authentication: bool,
18}
19
20#[derive(Debug, Deserialize, Validate)]
21struct UnlinkAccountRequest {
22    #[serde(rename = "accountId")]
23    account_id: String,
24}
25
26alibi_core::impl_auth_plugin! {
27    AccountManagementPlugin, "account-management";
28    routes {
29        get "/list-accounts" => handle_list_accounts, "list_accounts";
30        post "/unlink-account" => handle_unlink_account, "unlink_account";
31    }
32
33 extra {
34    fn static_openapi_metadata(&self) -> alibi_core::PluginOpenApiMetadata {
35        crate::metadata::plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self))
36    }
37
38    fn openapi_metadata(&self, ctx: &alibi_core::AuthInitContext<S>) -> alibi_core::PluginOpenApiMetadata {
39        crate::metadata::instance_plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self), ctx)
40    }
41 }
42}
43
44// ---------------------------------------------------------------------------
45// Old handler methods — delegate to core functions
46// ---------------------------------------------------------------------------
47
48impl AccountManagementPlugin {
49    async fn handle_list_accounts(
50        &self,
51        req: &AuthRequest,
52        ctx: &AuthContext<impl alibi_core::AuthSchema>,
53    ) -> AuthResult<AuthResponse> {
54        let (user, _session) =
55            super::organization::handlers::extension_common::session(req, ctx).await?;
56        let filtered = list_accounts_core(&user, ctx).await?;
57        Ok(AuthResponse::json(200, &filtered)?)
58    }
59
60    async fn handle_unlink_account(
61        &self,
62        req: &AuthRequest,
63        ctx: &AuthContext<impl alibi_core::AuthSchema>,
64    ) -> AuthResult<AuthResponse> {
65        let (user, _session) = ctx.require_cached_session(req).await?;
66
67        let unlink_req: UnlinkAccountRequest = match alibi_core::validate_request_body(req) {
68            Ok(v) => v,
69            Err(resp) => return Ok(resp),
70        };
71
72        let response = unlink_account_core(&user, &unlink_req.account_id, ctx).await?;
73        Ok(AuthResponse::json(200, &response)?)
74    }
75}
76
77impl std::fmt::Debug for AccountManagementPlugin {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("AccountManagementPlugin")
80            .finish_non_exhaustive()
81    }
82}
83
84// ---------------------------------------------------------------------------
85// Core functions — framework-agnostic business logic
86// ---------------------------------------------------------------------------
87
88///
89/// # Errors
90/// Returns an error when validation, storage, or an application callback fails.
91pub(in crate::plugins) async fn list_accounts_core(
92    user: &impl AuthUser,
93    ctx: &AuthContext<impl alibi_core::AuthSchema>,
94) -> AuthResult<Vec<serde_json::Map<String, serde_json::Value>>> {
95    let accounts = ctx.database.get_user_accounts_record(&user.id()).await?;
96    accounts
97        .iter()
98        .map(|account| {
99            let mut output = ctx.account_view(account)?;
100            let scopes = match output.remove("scope") {
101                None | Some(serde_json::Value::Null) => Vec::new(),
102                Some(serde_json::Value::String(scope)) => scope
103                    .split(',')
104                    .map(str::trim)
105                    .filter(|scope| !scope.is_empty())
106                    .map(str::to_owned)
107                    .collect(),
108                Some(_) => return Err(AuthError::internal("Account scope must be a string")),
109            };
110            drop(output.insert("scopes".into(), serde_json::to_value(scopes)?));
111            Ok(output)
112        })
113        .collect()
114}
115
116///
117/// # Errors
118/// Returns an error when validation, storage, or an application callback fails.
119pub(in crate::plugins) async fn unlink_account_core(
120    user: &impl AuthUser,
121    account_id: &str,
122    ctx: &AuthContext<impl alibi_core::AuthSchema>,
123) -> AuthResult<StatusResponse> {
124    let accounts = ctx.database.get_user_accounts(&user.id()).await?;
125    if accounts.len() == 1 && !ctx.config.account.account_linking.allow_unlinking_all {
126        return Err(AuthError::bad_request("You can't unlink your last account"));
127    }
128    let account = accounts
129        .iter()
130        .find(|account| account.id() == account_id)
131        .ok_or_else(|| AuthError::bad_request("Account not found"))?;
132    ctx.database.delete_account(&account.id()).await?;
133    Ok(StatusResponse { status: true })
134}