better_auth_api/plugins/
account_management.rs1use serde::Deserialize;
2
3use better_auth_core::entity::AuthUser;
4use better_auth_core::{AuthContext, AuthError, AuthResult};
5use better_auth_core::{AuthRequest, AuthResponse};
6
7use super::StatusResponse;
8
9pub struct AccountManagementPlugin {
11 config: AccountManagementConfig,
12}
13
14#[derive(Debug, Clone, better_auth_core::PluginConfig)]
15#[plugin(name = "AccountManagementPlugin")]
16pub struct AccountManagementConfig {
17 #[config(default = true)]
18 pub require_authentication: bool,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22struct UnlinkAccountRequest {
23 #[serde(rename = "accountId")]
24 account_id: String,
25}
26
27fn unlink_account_body(
28 req: &AuthRequest,
29) -> AuthResult<better_auth_core::endpoint_input::ValidatedBody> {
30 let (typed, projection) =
31 super::json_body::string_input::<UnlinkAccountRequest>(req, &[("accountId", true)])?;
32 Ok(better_auth_core::endpoint_input::ValidatedBody::new(
33 Some(projection),
34 typed,
35 ))
36}
37
38pub(crate) type AccountResponse = better_auth_core::FieldMap;
39
40better_auth_core::impl_auth_plugin! {
41 AccountManagementPlugin, "account-management";
42 routes {
43 get "/list-accounts" => handle_list_accounts, "listUserAccounts";
44 post "/unlink-account" => handle_unlink_account, "unlinkAccount", body = unlink_account_body;
45 }
46 extra {
47 fn telemetry_plugin_id(&self) -> Option<&'static str> {
48 None
49 }
50 }
51}
52
53pub(crate) async fn list_accounts_core(
58 user: &impl AuthUser,
59 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
60) -> AuthResult<Vec<AccountResponse>> {
61 let accounts = ctx.database.get_user_accounts(user.id().typed()?).await?;
62
63 accounts
64 .into_iter()
65 .map(|account| {
66 let mut fields = account.internal_fields()?;
67 ctx.config
68 .account
69 .field_schema()
70 .filter_returned_fields(&mut fields);
71 let scopes = match fields.remove("scope") {
72 Some(value) if better_auth_core::user_fields::is_truthy(&value) => {
73 let scope = value.as_str().ok_or_else(|| {
74 AuthError::internal("account.scope.split is not a function")
75 })?;
76 super::helpers::parse_stored_scopes(Some(scope))
77 }
78 _ => Vec::new(),
79 };
80 for name in [
82 "accessToken",
83 "refreshToken",
84 "idToken",
85 "accessTokenExpiresAt",
86 "refreshTokenExpiresAt",
87 "password",
88 "scope",
89 ] {
90 let _ = fields.remove(name);
91 }
92 let _ = fields.insert(
93 "scopes".into(),
94 scopes
95 .into_iter()
96 .map(Into::into)
97 .collect::<Vec<better_auth_core::FieldValue>>()
98 .into(),
99 );
100 Ok(fields)
101 })
102 .collect()
103}
104
105pub(crate) async fn unlink_account_core(
106 user: &impl AuthUser,
107 account_id: &str,
108 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
109) -> AuthResult<StatusResponse> {
110 let accounts = ctx.database.get_user_accounts(user.id().typed()?).await?;
111 if accounts.len() == 1 && !ctx.config.account.account_linking.allow_unlinking_all() {
112 return Err(AuthError::bad_request("You can't unlink your last account"));
113 }
114 let account = accounts
115 .iter()
116 .find(|account| account.id == account_id)
117 .ok_or_else(|| AuthError::bad_request("Account not found"))?;
118 ctx.database.delete_account(account.id.typed()?).await?;
119 Ok(StatusResponse { status: true })
120}
121
122impl AccountManagementPlugin {
127 async fn handle_list_accounts(
128 &self,
129 req: &AuthRequest,
130 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
131 ) -> AuthResult<AuthResponse> {
132 let (user, _session) = ctx.require_session(req).await?;
133 let filtered = list_accounts_core(&user, ctx).await?;
134 let response = filtered
135 .iter()
136 .map(better_auth_core::FieldMap::json)
137 .collect::<AuthResult<Vec<_>>>()?;
138 Ok(AuthResponse::json(200, &response)?)
139 }
140
141 async fn handle_unlink_account(
142 &self,
143 req: &AuthRequest,
144 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
145 ) -> AuthResult<AuthResponse> {
146 let (user, session) = ctx.require_session(req).await?;
147 if !crate::plugins::helpers::session_is_fresh(&session, &ctx.config) {
148 return Err(AuthError::Upstream {
149 status: 403,
150 code: "SESSION_NOT_FRESH",
151 message: "Session is not fresh",
152 });
153 }
154
155 let unlink_req: UnlinkAccountRequest = match req.validated_body::<UnlinkAccountRequest>() {
156 Some(body) => body.clone(),
157 None => super::json_body::string_input(req, &[("accountId", true)])?.0,
158 };
159
160 let response = unlink_account_core(&user, &unlink_req.account_id, ctx).await?;
161 Ok(AuthResponse::json(200, &response)?)
162 }
163}