alibi-plugins 0.4.1

Built-in authentication plugins and API metadata for Alibi
Documentation
use super::StatusResponse;
use super::authentication_helpers::{JsonField, RequestBody, parse_body};
use alibi_core::entity::{AuthAccount, AuthUser};
use alibi_core::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult};
use serde::Deserialize;
use validator::Validate;

/// Account management plugin for listing and unlinking user accounts.
pub struct AccountManagementPlugin {
    config: AccountManagementConfig,
}

#[derive(Debug, Clone, alibi_core::PluginConfig)]
#[plugin(name = "AccountManagementPlugin")]
pub struct AccountManagementConfig {
    #[config(default = true)]
    pub require_authentication: bool,
}

#[derive(Debug, Deserialize, Validate)]
struct UnlinkAccountRequest {
    #[serde(rename = "accountId")]
    account_id: String,
}

impl RequestBody for UnlinkAccountRequest {
    const FIELDS: &'static [JsonField] = &[JsonField::string("accountId", true)];
}

alibi_core::impl_auth_plugin! {
    AccountManagementPlugin, "account-management";
    routes {
        get "/list-accounts" => handle_list_accounts, "list_accounts";
        post "/unlink-account" => handle_unlink_account, "unlink_account";
    }

 extra {
    route_openapi_metadata!(S);
 }
}

impl AccountManagementPlugin {
    async fn handle_list_accounts(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<impl alibi_core::AuthSchema>,
    ) -> AuthResult<AuthResponse> {
        let (user, _session) =
            super::organization::handlers::extension_common::session(req, ctx).await?;
        let filtered = list_accounts_core(&user, ctx).await?;
        Ok(AuthResponse::json(200, &filtered)?)
    }

    async fn handle_unlink_account(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<impl alibi_core::AuthSchema>,
    ) -> AuthResult<AuthResponse> {
        let (user, _session) = ctx.require_cached_session(req).await?;

        let unlink_req: UnlinkAccountRequest = match parse_body(req) {
            Ok(v) => v,
            Err(resp) => return Ok(resp),
        };

        let response = unlink_account_core(&user, &unlink_req.account_id, ctx).await?;
        Ok(AuthResponse::json(200, &response)?)
    }
}

impl std::fmt::Debug for AccountManagementPlugin {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AccountManagementPlugin")
            .finish_non_exhaustive()
    }
}

pub(crate) async fn list_accounts_core(
    user: &impl AuthUser,
    ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<Vec<serde_json::Map<String, serde_json::Value>>> {
    let accounts = ctx.database.get_user_accounts_record(&user.id()).await?;
    accounts
        .iter()
        .map(|account| {
            let mut output = ctx.account_view(account)?;
            let scopes = match output.remove("scope") {
                None | Some(serde_json::Value::Null) => Vec::new(),
                Some(serde_json::Value::String(scope)) => scope
                    .split(',')
                    .map(str::trim)
                    .filter(|scope| !scope.is_empty())
                    .map(str::to_owned)
                    .collect(),
                Some(_) => return Err(AuthError::internal("Account scope must be a string")),
            };
            _ = output.insert("scopes".into(), serde_json::to_value(scopes)?);
            Ok(output)
        })
        .collect()
}

pub(crate) async fn unlink_account_core(
    user: &impl AuthUser,
    account_id: &str,
    ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<StatusResponse> {
    let accounts = ctx.database.get_user_accounts(&user.id()).await?;
    if accounts.len() == 1 && !ctx.config.account.account_linking.allow_unlinking_all {
        return Err(AuthError::bad_request("You can't unlink your last account"));
    }
    let account = accounts
        .iter()
        .find(|account| account.id() == account_id)
        .ok_or_else(|| AuthError::bad_request("Account not found"))?;
    ctx.database.delete_account(&account.id()).await?;
    Ok(StatusResponse { status: true })
}