better_auth_api/plugins/organization/
server_api.rs1use super::{OrganizationPlugin, hooks::*, types::RoleInput};
2use crate::plugins::endpoint_context::EndpointContext;
3use better_auth_core::entity::{AuthSession, AuthUser};
4use better_auth_core::{
5 AuthContext, AuthError, AuthRequest, AuthResult, AuthSchema, FieldMap, FieldValue,
6 FromFieldMap, Member,
7};
8mod native;
9mod store;
10pub use native::OrganizationApi;
11
12#[cfg(test)]
13#[path = "server_api_input_tests.rs"]
14mod input_tests;
15
16#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct AddMemberInput {
20 #[serde(flatten)]
21 #[serde(with = "better_auth_core::field_value::serde::map")]
22 pub additional_fields: better_auth_core::FieldMap,
23 #[serde(
24 default,
25 skip_serializing_if = "better_auth_core::SchemaValue::is_undefined"
26 )]
27 pub user_id: better_auth_core::SchemaValue<String>,
28 #[serde(
29 default,
30 skip_serializing_if = "better_auth_core::SchemaValue::is_undefined"
31 )]
32 pub organization_id: better_auth_core::SchemaValue<String>,
33 #[serde(
34 default,
35 skip_serializing_if = "better_auth_core::SchemaValue::is_undefined"
36 )]
37 pub role: better_auth_core::SchemaValue<RoleInput>,
38 #[serde(
39 default,
40 skip_serializing_if = "better_auth_core::SchemaValue::is_undefined"
41 )]
42 pub team_id: better_auth_core::SchemaValue<String>,
43}
44
45super::request::from_fields!(AddMemberInput {
46 user_id: "userId", organization_id: "organizationId", role: "role", team_id: "teamId",
47}; additional_fields);
48
49impl AddMemberInput {
50 fn into_field_values(self) -> FieldMap {
51 let mut fields = self.additional_fields;
52 for (name, value) in [
53 ("userId", self.user_id.field_value()),
54 ("organizationId", self.organization_id.field_value()),
55 ("role", self.role.field_value()),
56 ("teamId", self.team_id.field_value()),
57 ] {
58 if !value.is_undefined() {
59 let _ = fields.insert(name.into(), value);
60 }
61 }
62 fields
63 }
64}
65
66impl OrganizationPlugin {
67 pub async fn add_member<S: AuthSchema>(
69 &self,
70 input: AddMemberInput,
71 request: Option<&AuthRequest>,
72 ctx: &AuthContext<S>,
73 ) -> AuthResult<Member> {
74 let response = OrganizationApi::with_plugin(self.clone(), ctx)
75 .with_request(better_auth_core::NativeRequest {
76 request,
77 headers: request.map(|request| &request.headers),
78 })
79 .add_member_value(input.into_field_values().into())
80 .await?;
81 match response {
82 FieldValue::Object(fields) => Member::from_field_values((*fields).clone()),
83 _ => Err(AuthError::internal(
84 "Native addMember did not return a member object",
85 )),
86 }
87 }
88
89 async fn add_member_core<S: AuthSchema>(
90 &self,
91 input: AddMemberInput,
92 request: &AuthRequest,
93 endpoint: &EndpointContext<'_, S>,
94 ) -> AuthResult<Member> {
95 let ctx = endpoint.auth;
96 let store = store::MemberAdapter::new(endpoint);
97 let additional_fields = input.additional_fields.clone();
98 let user_id = input.user_id.field_value();
99 let user_id = user_id.is_truthy().then_some(user_id);
100 let session = match user_id.is_some() {
102 true => super::handlers::require_session(request, ctx).await.ok(),
103 _ => None,
104 };
105 let org_value = input.organization_id.field_value();
106 let org_value = org_value
107 .is_truthy()
108 .then_some(org_value)
109 .or_else(|| {
110 session
111 .as_ref()
112 .and_then(|(_, session)| session.active_organization_id())
113 .map(better_auth_core::FieldValue::from)
114 })
115 .ok_or(AuthError::Upstream {
116 status: 400,
117 code: "NO_ACTIVE_ORGANIZATION",
118 message: "No active organization",
119 })?;
120 let team_value = input.team_id.field_value();
121 let team_value = team_value.is_truthy().then_some(team_value);
122 if team_value.is_some() && !self.config.teams.enabled {
123 ctx.config.logger.error("Teams are not enabled", &[]);
124 return Err(better_auth_core::AuthResponse::json(
125 400,
126 &serde_json::json!({"message":"Teams are not enabled"}),
127 )?
128 .into());
129 }
130 let user = store
131 .get_user_by_id_value(&user_id.ok_or(AuthError::Upstream {
132 status: 400,
133 code: "USER_NOT_FOUND",
134 message: "User not found",
135 })?)
136 .await?
137 .ok_or(AuthError::Upstream {
138 status: 400,
139 code: "USER_NOT_FOUND",
140 message: "User not found",
141 })?;
142 if let Some(email) = user.email()
143 && let Some(existing) = store.get_user_by_email(email).await?
144 && store
145 .get_member_value(&org_value, &existing.id().field_value())
146 .await?
147 .is_some()
148 {
149 return Err(AuthError::Upstream {
150 status: 400,
151 code: "USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION",
152 message: "User is already a member of this organization",
153 });
154 }
155 let team = if let Some(team_id) = &team_value {
156 let team = store
157 .get_team_value(team_id)
158 .await?
159 .ok_or(AuthError::Upstream {
160 status: 400,
161 code: "TEAM_NOT_FOUND",
162 message: "Team not found",
163 })?;
164 if !team.organization_id.field_value().strict_equals(&org_value) {
165 return Err(AuthError::Upstream {
166 status: 400,
167 code: "TEAM_NOT_FOUND",
168 message: "Team not found",
169 });
170 }
171 Some(team)
172 } else {
173 None
174 };
175 let count = store.count_organization_members(&org_value).await?;
176 let organization = store
177 .get_organization_by_id_value(&org_value)
178 .await?
179 .ok_or(AuthError::Upstream {
180 status: 400,
181 code: "ORGANIZATION_NOT_FOUND",
182 message: "Organization not found",
183 })?;
184 let org_id = organization.id.typed()?.as_str();
185 let organization_view =
186 crate::plugins::organization::fields::organization(&organization, ctx);
187 let user_view = ctx.internal_user_view(&user).await?;
188 let event = OrganizationUser {
189 user: &user_view,
190 organization: &organization_view,
191 };
192 if count >= self.config.member_limit(event).await? as i64 {
193 return Err(AuthError::Upstream {
194 status: 403,
195 code: "ORGANIZATION_MEMBERSHIP_LIMIT_REACHED",
196 message: "Organization membership limit reached",
197 });
198 }
199 let mut data = OrganizationMemberDraft {
200 additional_fields,
201 organization_id: org_id.to_owned().into(),
202 user_id: user.id().into_owned(),
203 role: super::input::parse_roles(&input.role)?,
204 team_id: team
205 .as_ref()
206 .map(|team| team.id.typed().cloned())
207 .transpose()?,
208 created_at: None,
209 };
210 if let Some(hooks) = &self.config.hooks {
211 hooks.before_add_member(&mut data, event).await?;
212 }
213 let member = store.create_member(data.into_create()).await?;
214 if let Some(team) = team {
215 let team_id = team.id.typed()?.clone();
216 let result = async {
217 let maximum = if let Some((actor, session)) = &session {
218 let user_view = ctx.user_view(actor).await?;
219 let session_view = ctx.session_view(session).await?;
220 self.config
221 .team_member_limit(OrganizationTeamMemberLimit {
222 team_id: &team_id,
223 organization_id: org_id,
224 session: OrganizationSession {
225 user: &user_view,
226 session: &session_view,
227 },
228 })
229 .await?
230 } else if self
231 .config
232 .teams
233 .maximum_members_per_team_callback
234 .is_some()
235 {
236 return Err(better_auth_core::AuthResponse::new(401).into());
237 } else {
238 self.config.teams.maximum_members_per_team
239 };
240 let _ = store
241 .add_team_member(&team_id.into(), user.id().typed()?, maximum)
242 .await?
243 .ok_or(AuthError::Upstream {
244 status: 403,
245 code: "TEAM_MEMBER_LIMIT_REACHED",
246 message: "Team member limit reached",
247 })?;
248 AuthResult::Ok(())
249 }
250 .await;
251 if let Err(error) = result {
252 store
253 .delete_member_for_user(member.id.typed()?, org_id, user.id().typed()?)
254 .await?;
255 return Err(error);
256 }
257 }
258 if let Some(hooks) = &self.config.hooks {
259 hooks
260 .after_add_member(OrganizationMemberEvent {
261 member: &member,
262 user: &user_view,
263 organization: &organization_view,
264 })
265 .await?;
266 }
267 Ok(member)
268 }
269}