alibi_api/plugins/
account_management.rs1use 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
8pub 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
44impl 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
84pub(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
116pub(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}