Skip to main content

better_auth_api/plugins/organization/
server_api.rs

1use 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/// Trusted server-side member creation. This endpoint has no HTTP route upstream.
17#[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    /// Add a member from a trusted server context, optionally using an authenticated request.
68    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        // Upstream permits a supplied user ID even when session lookup fails.
101        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}