Skip to main content

alibi_plugins/
account_management.rs

1use super::StatusResponse;
2use super::authentication_helpers::{JsonField, RequestBody, parse_body};
3use alibi_core::entity::{AuthAccount, AuthUser};
4use alibi_core::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult};
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
26impl RequestBody for UnlinkAccountRequest {
27    const FIELDS: &'static [JsonField] = &[JsonField::string("accountId", true)];
28}
29
30alibi_core::impl_auth_plugin! {
31    AccountManagementPlugin, "account-management";
32    routes {
33        get "/list-accounts" => handle_list_accounts, "list_accounts";
34        post "/unlink-account" => handle_unlink_account, "unlink_account";
35    }
36
37 extra {
38    route_openapi_metadata!(S);
39 }
40}
41
42impl AccountManagementPlugin {
43    async fn handle_list_accounts(
44        &self,
45        req: &AuthRequest,
46        ctx: &AuthContext<impl alibi_core::AuthSchema>,
47    ) -> AuthResult<AuthResponse> {
48        let (user, _session) =
49            super::organization::handlers::extension_common::session(req, ctx).await?;
50        let filtered = list_accounts_core(&user, ctx).await?;
51        Ok(AuthResponse::json(200, &filtered)?)
52    }
53
54    async fn handle_unlink_account(
55        &self,
56        req: &AuthRequest,
57        ctx: &AuthContext<impl alibi_core::AuthSchema>,
58    ) -> AuthResult<AuthResponse> {
59        let (user, _session) = ctx.require_cached_session(req).await?;
60
61        let unlink_req: UnlinkAccountRequest = match parse_body(req) {
62            Ok(v) => v,
63            Err(resp) => return Ok(resp),
64        };
65
66        let response = unlink_account_core(&user, &unlink_req.account_id, ctx).await?;
67        Ok(AuthResponse::json(200, &response)?)
68    }
69}
70
71impl std::fmt::Debug for AccountManagementPlugin {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("AccountManagementPlugin")
74            .finish_non_exhaustive()
75    }
76}
77
78pub(crate) async fn list_accounts_core(
79    user: &impl AuthUser,
80    ctx: &AuthContext<impl alibi_core::AuthSchema>,
81) -> AuthResult<Vec<serde_json::Map<String, serde_json::Value>>> {
82    let accounts = ctx.database.get_user_accounts_record(&user.id()).await?;
83    accounts
84        .iter()
85        .map(|account| {
86            let mut output = ctx.account_view(account)?;
87            let scopes = match output.remove("scope") {
88                None | Some(serde_json::Value::Null) => Vec::new(),
89                Some(serde_json::Value::String(scope)) => scope
90                    .split(',')
91                    .map(str::trim)
92                    .filter(|scope| !scope.is_empty())
93                    .map(str::to_owned)
94                    .collect(),
95                Some(_) => return Err(AuthError::internal("Account scope must be a string")),
96            };
97            _ = output.insert("scopes".into(), serde_json::to_value(scopes)?);
98            Ok(output)
99        })
100        .collect()
101}
102
103pub(crate) async fn unlink_account_core(
104    user: &impl AuthUser,
105    account_id: &str,
106    ctx: &AuthContext<impl alibi_core::AuthSchema>,
107) -> AuthResult<StatusResponse> {
108    let accounts = ctx.database.get_user_accounts(&user.id()).await?;
109    if accounts.len() == 1 && !ctx.config.account.account_linking.allow_unlinking_all {
110        return Err(AuthError::bad_request("You can't unlink your last account"));
111    }
112    let account = accounts
113        .iter()
114        .find(|account| account.id() == account_id)
115        .ok_or_else(|| AuthError::bad_request("Account not found"))?;
116    ctx.database.delete_account(&account.id()).await?;
117    Ok(StatusResponse { status: true })
118}