use better_auth_core::adapters::DatabaseAdapter;
use better_auth_core::entity::{AuthMember, AuthSession, AuthUser};
use better_auth_core::error::{AuthError, AuthResult};
use better_auth_core::plugin::AuthContext;
use better_auth_core::types::{AuthRequest, AuthResponse};
use super::{require_session, resolve_organization_id};
use crate::plugins::organization::OrganizationConfig;
use crate::plugins::organization::rbac::{Action, Resource, has_permission_any};
use crate::plugins::organization::types::{
ListMembersQuery, ListMembersResponse, MemberResponse, MemberWrappedResponse,
RemoveMemberRequest, RemovedMemberInfo, RemovedMemberResponse, UpdateMemberRoleRequest,
};
pub(crate) async fn get_active_member_core<DB: DatabaseAdapter>(
user: &DB::User,
session: &DB::Session,
ctx: &AuthContext<DB>,
) -> AuthResult<MemberResponse> {
let org_id = session
.active_organization_id()
.ok_or_else(|| AuthError::bad_request("No active organization"))?;
let member = ctx
.database
.get_member(org_id, user.id())
.await?
.ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;
Ok(MemberResponse::from_member_and_user(&member, user))
}
pub(crate) async fn list_members_core<DB: DatabaseAdapter>(
query: &ListMembersQuery,
user: &DB::User,
session: &DB::Session,
ctx: &AuthContext<DB>,
) -> AuthResult<ListMembersResponse> {
let org_id = resolve_organization_id(
query.organization_id.as_deref(),
query.organization_slug.as_deref(),
session,
ctx,
)
.await?;
ctx.database
.get_member(&org_id, user.id())
.await?
.ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;
let members_raw = ctx.database.list_organization_members(&org_id).await?;
let total = members_raw.len();
let offset = query.offset.unwrap_or(0);
let limit = query.limit.unwrap_or(50).min(100);
let members_page: Vec<_> = members_raw.into_iter().skip(offset).take(limit).collect();
let mut members = Vec::with_capacity(members_page.len());
for member in &members_page {
if let Some(user_info) = ctx.database.get_user_by_id(member.user_id()).await? {
members.push(MemberResponse::from_member_and_user(member, &user_info));
}
}
Ok(ListMembersResponse { members, total })
}
pub(crate) async fn remove_member_core<DB: DatabaseAdapter>(
body: &RemoveMemberRequest,
user: &DB::User,
session: &DB::Session,
config: &OrganizationConfig,
ctx: &AuthContext<DB>,
) -> AuthResult<RemovedMemberResponse> {
let org_id =
resolve_organization_id(body.organization_id.as_deref(), None, session, ctx).await?;
let requester_member = ctx
.database
.get_member(&org_id, user.id())
.await?
.ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;
let target_member_id: String;
let target_member_org_id: String;
let target_member_user_id: String;
let target_member_role: String;
if let Some(member_id) = &body.member_id {
let target = ctx
.database
.get_member_by_id(member_id)
.await?
.ok_or_else(|| AuthError::not_found("Member not found"))?;
target_member_id = target.id().to_string();
target_member_org_id = target.organization_id().to_string();
target_member_user_id = target.user_id().to_string();
target_member_role = target.role().to_string();
} else if let Some(email) = &body.email {
let target_user = ctx
.database
.get_user_by_email(email)
.await?
.ok_or_else(|| AuthError::not_found("User not found"))?;
let target = ctx
.database
.get_member(&org_id, target_user.id())
.await?
.ok_or_else(|| AuthError::not_found("Member not found"))?;
target_member_id = target.id().to_string();
target_member_org_id = target.organization_id().to_string();
target_member_user_id = target.user_id().to_string();
target_member_role = target.role().to_string();
} else {
return Err(AuthError::bad_request(
"Either memberId or email must be provided",
));
};
if target_member_org_id != org_id {
return Err(AuthError::bad_request("Member not in this organization"));
}
let is_self_removal = target_member_user_id == user.id();
if !is_self_removal
&& !has_permission_any(
requester_member.role(),
&Resource::Member,
&Action::Delete,
&config.roles,
)
{
return Err(AuthError::forbidden(
"You don't have permission to remove members",
));
}
if target_member_role.contains("owner") {
let all_members = ctx.database.list_organization_members(&org_id).await?;
let owner_count = all_members
.iter()
.filter(|m| m.role().contains("owner"))
.count();
if owner_count <= 1 {
return Err(AuthError::bad_request(
"Cannot remove the last owner from an organization",
));
}
}
let response = RemovedMemberResponse {
member: RemovedMemberInfo {
id: target_member_id.clone(),
user_id: target_member_user_id,
organization_id: target_member_org_id,
role: target_member_role,
},
};
ctx.database.delete_member(&target_member_id).await?;
Ok(response)
}
pub(crate) async fn update_member_role_core<DB: DatabaseAdapter>(
body: &UpdateMemberRoleRequest,
user: &DB::User,
session: &DB::Session,
config: &OrganizationConfig,
ctx: &AuthContext<DB>,
) -> AuthResult<MemberWrappedResponse> {
let org_id =
resolve_organization_id(body.organization_id.as_deref(), None, session, ctx).await?;
let requester_member = ctx
.database
.get_member(&org_id, user.id())
.await?
.ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;
if !has_permission_any(
requester_member.role(),
&Resource::Member,
&Action::Update,
&config.roles,
) {
return Err(AuthError::forbidden(
"You don't have permission to update member roles",
));
}
let target_member = ctx
.database
.get_member_by_id(&body.member_id)
.await?
.ok_or_else(|| AuthError::not_found("Member not found"))?;
if target_member.organization_id() != org_id {
return Err(AuthError::bad_request("Member not in this organization"));
}
if target_member.role().contains("owner") && !body.role.contains("owner") {
let all_members = ctx.database.list_organization_members(&org_id).await?;
let owner_count = all_members
.iter()
.filter(|m| m.role().contains("owner"))
.count();
if owner_count <= 1 {
return Err(AuthError::bad_request(
"Cannot demote the last owner. Transfer ownership first.",
));
}
}
let updated = ctx
.database
.update_member_role(&body.member_id, &body.role)
.await?;
let user_info = ctx
.database
.get_user_by_id(updated.user_id())
.await?
.ok_or_else(|| AuthError::internal("User not found for updated member"))?;
Ok(MemberWrappedResponse {
member: MemberResponse::from_member_and_user(&updated, &user_info),
})
}
pub async fn handle_get_active_member<DB: DatabaseAdapter>(
req: &AuthRequest,
ctx: &AuthContext<DB>,
) -> AuthResult<AuthResponse> {
let (user, session) = require_session(req, ctx).await?;
let response = get_active_member_core(&user, &session, ctx).await?;
Ok(AuthResponse::json(200, &response)?)
}
pub async fn handle_list_members<DB: DatabaseAdapter>(
req: &AuthRequest,
ctx: &AuthContext<DB>,
) -> AuthResult<AuthResponse> {
let (user, session) = require_session(req, ctx).await?;
let query = parse_query::<ListMembersQuery>(&req.query);
let response = list_members_core(&query, &user, &session, ctx).await?;
Ok(AuthResponse::json(200, &response)?)
}
pub async fn handle_remove_member<DB: DatabaseAdapter>(
req: &AuthRequest,
ctx: &AuthContext<DB>,
config: &OrganizationConfig,
) -> AuthResult<AuthResponse> {
let (user, session) = require_session(req, ctx).await?;
let body: RemoveMemberRequest = match better_auth_core::validate_request_body(req) {
Ok(v) => v,
Err(resp) => return Ok(resp),
};
let response = remove_member_core(&body, &user, &session, config, ctx).await?;
Ok(AuthResponse::json(200, &response)?)
}
pub async fn handle_update_member_role<DB: DatabaseAdapter>(
req: &AuthRequest,
ctx: &AuthContext<DB>,
config: &OrganizationConfig,
) -> AuthResult<AuthResponse> {
let (user, session) = require_session(req, ctx).await?;
let body: UpdateMemberRoleRequest = match better_auth_core::validate_request_body(req) {
Ok(v) => v,
Err(resp) => return Ok(resp),
};
let response = update_member_role_core(&body, &user, &session, config, ctx).await?;
Ok(AuthResponse::json(200, &response)?)
}
fn parse_query<T: Default + serde::de::DeserializeOwned>(
query: &std::collections::HashMap<String, String>,
) -> T {
let json_value =
serde_json::to_value(query).unwrap_or(serde_json::Value::Object(Default::default()));
serde_json::from_value(json_value).unwrap_or_default()
}