Skip to main content

better_auth_api/plugins/organization/
mod.rs

1pub mod handlers;
2pub mod rbac;
3pub mod types;
4
5use std::collections::HashMap;
6
7use async_trait::async_trait;
8use better_auth_core::adapters::DatabaseAdapter;
9use better_auth_core::error::AuthResult;
10use better_auth_core::plugin::{AuthContext, AuthPlugin, AuthRoute};
11use better_auth_core::types::{AuthRequest, AuthResponse, HttpMethod};
12
13#[cfg(feature = "axum")]
14use better_auth_core::plugin::{AuthState, AxumPlugin};
15
16/// Permission definitions for a role
17#[derive(Debug, Clone, Default)]
18pub struct RolePermissions {
19    pub organization: Vec<String>,
20    pub member: Vec<String>,
21    pub invitation: Vec<String>,
22}
23
24/// Configuration for the Organization plugin
25#[derive(Debug, Clone, better_auth_core::PluginConfig)]
26#[plugin(name = "OrganizationPlugin")]
27pub struct OrganizationConfig {
28    /// Allow users to create organizations (default: true)
29    #[config(default = true)]
30    pub allow_user_to_create_organization: bool,
31    /// Maximum organizations per user (None = unlimited)
32    #[config(default = None)]
33    pub organization_limit: Option<usize>,
34    /// Maximum members per organization (None = unlimited)
35    #[config(default = Some(100))]
36    pub membership_limit: Option<usize>,
37    /// Role assigned to organization creator (default: "owner")
38    #[config(default = "owner".to_string())]
39    pub creator_role: String,
40    /// Invitation expiration in seconds (default: 48 hours)
41    #[config(default = 60 * 60 * 48)]
42    pub invitation_expires_in: u64,
43    /// Maximum pending invitations per organization (None = unlimited)
44    #[config(default = Some(100))]
45    pub invitation_limit: Option<usize>,
46    /// Disable organization deletion (default: false)
47    #[config(default = false)]
48    pub disable_organization_deletion: bool,
49    /// Custom role definitions (extending default roles)
50    #[config(default = HashMap::new(), skip)]
51    pub roles: HashMap<String, RolePermissions>,
52}
53
54/// Organization plugin for multi-tenancy support
55pub struct OrganizationPlugin {
56    config: OrganizationConfig,
57}
58
59#[async_trait]
60impl<DB: DatabaseAdapter> AuthPlugin<DB> for OrganizationPlugin {
61    fn name(&self) -> &'static str {
62        "organization"
63    }
64
65    fn routes(&self) -> Vec<AuthRoute> {
66        vec![
67            // Organization CRUD
68            AuthRoute::post("/organization/create", "create_organization"),
69            AuthRoute::post("/organization/update", "update_organization"),
70            AuthRoute::post("/organization/delete", "delete_organization"),
71            AuthRoute::get("/organization/list", "list_organizations"),
72            AuthRoute::get(
73                "/organization/get-full-organization",
74                "get_full_organization",
75            ),
76            AuthRoute::post("/organization/check-slug", "check_slug"),
77            AuthRoute::post("/organization/set-active", "set_active_organization"),
78            AuthRoute::post("/organization/leave", "leave_organization"),
79            // Member management
80            AuthRoute::get("/organization/get-active-member", "get_active_member"),
81            AuthRoute::get("/organization/list-members", "list_members"),
82            AuthRoute::post("/organization/remove-member", "remove_member"),
83            AuthRoute::post("/organization/update-member-role", "update_member_role"),
84            // Invitations
85            AuthRoute::post("/organization/invite-member", "invite_member"),
86            AuthRoute::get("/organization/get-invitation", "get_invitation"),
87            AuthRoute::get("/organization/list-invitations", "list_invitations"),
88            AuthRoute::get(
89                "/organization/list-user-invitations",
90                "list_user_invitations",
91            ),
92            AuthRoute::post("/organization/accept-invitation", "accept_invitation"),
93            AuthRoute::post("/organization/reject-invitation", "reject_invitation"),
94            AuthRoute::post("/organization/cancel-invitation", "cancel_invitation"),
95            // Permission check
96            AuthRoute::post("/organization/has-permission", "has_permission"),
97        ]
98    }
99
100    async fn on_request(
101        &self,
102        req: &AuthRequest,
103        ctx: &AuthContext<DB>,
104    ) -> AuthResult<Option<AuthResponse>> {
105        match (req.method(), req.path()) {
106            // Organization CRUD
107            (HttpMethod::Post, "/organization/create") => Ok(Some(
108                handlers::org::handle_create_organization(req, ctx, &self.config).await?,
109            )),
110            (HttpMethod::Post, "/organization/update") => Ok(Some(
111                handlers::org::handle_update_organization(req, ctx, &self.config).await?,
112            )),
113            (HttpMethod::Post, "/organization/delete") => Ok(Some(
114                handlers::org::handle_delete_organization(req, ctx, &self.config).await?,
115            )),
116            (HttpMethod::Get, "/organization/list") => Ok(Some(
117                handlers::org::handle_list_organizations(req, ctx).await?,
118            )),
119            (HttpMethod::Get, "/organization/get-full-organization") => Ok(Some(
120                handlers::org::handle_get_full_organization(req, ctx).await?,
121            )),
122            (HttpMethod::Post, "/organization/check-slug") => {
123                Ok(Some(handlers::org::handle_check_slug(req, ctx).await?))
124            }
125            (HttpMethod::Post, "/organization/set-active") => Ok(Some(
126                handlers::org::handle_set_active_organization(req, ctx).await?,
127            )),
128            (HttpMethod::Post, "/organization/leave") => Ok(Some(
129                handlers::org::handle_leave_organization(req, ctx).await?,
130            )),
131            // Member management
132            (HttpMethod::Get, "/organization/get-active-member") => Ok(Some(
133                handlers::member::handle_get_active_member(req, ctx).await?,
134            )),
135            (HttpMethod::Get, "/organization/list-members") => {
136                Ok(Some(handlers::member::handle_list_members(req, ctx).await?))
137            }
138            (HttpMethod::Post, "/organization/remove-member") => Ok(Some(
139                handlers::member::handle_remove_member(req, ctx, &self.config).await?,
140            )),
141            (HttpMethod::Post, "/organization/update-member-role") => Ok(Some(
142                handlers::member::handle_update_member_role(req, ctx, &self.config).await?,
143            )),
144            // Invitations
145            (HttpMethod::Post, "/organization/invite-member") => Ok(Some(
146                handlers::invitation::handle_invite_member(req, ctx, &self.config).await?,
147            )),
148            (HttpMethod::Get, "/organization/get-invitation") => Ok(Some(
149                handlers::invitation::handle_get_invitation(req, ctx).await?,
150            )),
151            (HttpMethod::Get, "/organization/list-invitations") => Ok(Some(
152                handlers::invitation::handle_list_invitations(req, ctx).await?,
153            )),
154            (HttpMethod::Get, "/organization/list-user-invitations") => Ok(Some(
155                handlers::invitation::handle_list_user_invitations(req, ctx).await?,
156            )),
157            (HttpMethod::Post, "/organization/accept-invitation") => Ok(Some(
158                handlers::invitation::handle_accept_invitation(req, ctx, &self.config).await?,
159            )),
160            (HttpMethod::Post, "/organization/reject-invitation") => Ok(Some(
161                handlers::invitation::handle_reject_invitation(req, ctx).await?,
162            )),
163            (HttpMethod::Post, "/organization/cancel-invitation") => Ok(Some(
164                handlers::invitation::handle_cancel_invitation(req, ctx, &self.config).await?,
165            )),
166            // Permission check
167            (HttpMethod::Post, "/organization/has-permission") => Ok(Some(
168                handlers::handle_has_permission(req, ctx, &self.config).await?,
169            )),
170            _ => Ok(None),
171        }
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Axum-native routing (feature-gated)
177// ---------------------------------------------------------------------------
178
179#[cfg(feature = "axum")]
180mod axum_impl {
181    use super::*;
182    use std::sync::Arc;
183
184    use axum::Json;
185    use axum::extract::{Extension, Query, State};
186    use better_auth_core::error::AuthError;
187    use better_auth_core::extractors::{CurrentSession, ValidatedJson};
188
189    use super::handlers::has_permission_core;
190    use super::handlers::invitation::{
191        accept_invitation_core, cancel_invitation_core, get_invitation_core, invite_member_core,
192        list_invitations_core, list_user_invitations_core, reject_invitation_core,
193    };
194    use super::handlers::member::{
195        get_active_member_core, list_members_core, remove_member_core, update_member_role_core,
196    };
197    use super::handlers::org::{
198        check_slug_core, create_organization_core, delete_organization_core,
199        get_full_organization_core, leave_organization_core, list_organizations_core,
200        set_active_organization_core, update_organization_core,
201    };
202    use super::types::*;
203
204    #[derive(Clone)]
205    struct PluginState {
206        config: OrganizationConfig,
207    }
208
209    // -- Organization CRUD --
210
211    async fn handle_create_organization<DB: DatabaseAdapter>(
212        State(state): State<AuthState<DB>>,
213        Extension(ps): Extension<Arc<PluginState>>,
214        CurrentSession { user, .. }: CurrentSession<DB>,
215        ValidatedJson(body): ValidatedJson<CreateOrganizationRequest>,
216    ) -> Result<Json<CreateOrganizationResponse<DB::Organization, MemberResponse>>, AuthError> {
217        let ctx = state.to_context();
218        let result = create_organization_core(&body, &user, &ps.config, &ctx).await?;
219        Ok(Json(result))
220    }
221
222    async fn handle_update_organization<DB: DatabaseAdapter>(
223        State(state): State<AuthState<DB>>,
224        Extension(ps): Extension<Arc<PluginState>>,
225        CurrentSession { user, session, .. }: CurrentSession<DB>,
226        ValidatedJson(body): ValidatedJson<UpdateOrganizationRequest>,
227    ) -> Result<Json<DB::Organization>, AuthError> {
228        let ctx = state.to_context();
229        let result = update_organization_core(&body, &user, &session, &ps.config, &ctx).await?;
230        Ok(Json(result))
231    }
232
233    async fn handle_delete_organization<DB: DatabaseAdapter>(
234        State(state): State<AuthState<DB>>,
235        Extension(ps): Extension<Arc<PluginState>>,
236        CurrentSession { user, .. }: CurrentSession<DB>,
237        Json(body): Json<DeleteOrganizationRequest>,
238    ) -> Result<Json<SuccessResponse>, AuthError> {
239        let ctx = state.to_context();
240        let result = delete_organization_core(&body, &user, &ps.config, &ctx).await?;
241        Ok(Json(result))
242    }
243
244    async fn handle_list_organizations<DB: DatabaseAdapter>(
245        State(state): State<AuthState<DB>>,
246        CurrentSession { user, .. }: CurrentSession<DB>,
247    ) -> Result<Json<Vec<DB::Organization>>, AuthError> {
248        let ctx = state.to_context();
249        let result = list_organizations_core(&user, &ctx).await?;
250        Ok(Json(result))
251    }
252
253    async fn handle_get_full_organization<DB: DatabaseAdapter>(
254        State(state): State<AuthState<DB>>,
255        CurrentSession { user, session, .. }: CurrentSession<DB>,
256        Query(query): Query<GetFullOrganizationQuery>,
257    ) -> Result<Json<FullOrganizationResponse<DB::Organization, DB::Invitation>>, AuthError> {
258        let ctx = state.to_context();
259        let result = get_full_organization_core(&query, &user, &session, &ctx).await?;
260        Ok(Json(result))
261    }
262
263    async fn handle_check_slug<DB: DatabaseAdapter>(
264        State(state): State<AuthState<DB>>,
265        CurrentSession { .. }: CurrentSession<DB>,
266        Json(body): Json<CheckSlugRequest>,
267    ) -> Result<Json<CheckSlugResponse>, AuthError> {
268        let ctx = state.to_context();
269        let result = check_slug_core(&body, &ctx).await?;
270        Ok(Json(result))
271    }
272
273    async fn handle_set_active_organization<DB: DatabaseAdapter>(
274        State(state): State<AuthState<DB>>,
275        CurrentSession { user, session, .. }: CurrentSession<DB>,
276        Json(body): Json<SetActiveOrganizationRequest>,
277    ) -> Result<Json<DB::Session>, AuthError> {
278        let ctx = state.to_context();
279        let result = set_active_organization_core(&body, &user, &session, &ctx).await?;
280        Ok(Json(result))
281    }
282
283    async fn handle_leave_organization<DB: DatabaseAdapter>(
284        State(state): State<AuthState<DB>>,
285        CurrentSession { user, session, .. }: CurrentSession<DB>,
286        Json(body): Json<LeaveOrganizationRequest>,
287    ) -> Result<Json<SuccessResponse>, AuthError> {
288        let ctx = state.to_context();
289        let result = leave_organization_core(&body, &user, &session, &ctx).await?;
290        Ok(Json(result))
291    }
292
293    // -- Member management --
294
295    async fn handle_get_active_member<DB: DatabaseAdapter>(
296        State(state): State<AuthState<DB>>,
297        CurrentSession { user, session, .. }: CurrentSession<DB>,
298    ) -> Result<Json<MemberResponse>, AuthError> {
299        let ctx = state.to_context();
300        let result = get_active_member_core(&user, &session, &ctx).await?;
301        Ok(Json(result))
302    }
303
304    async fn handle_list_members<DB: DatabaseAdapter>(
305        State(state): State<AuthState<DB>>,
306        CurrentSession { user, session, .. }: CurrentSession<DB>,
307        Query(query): Query<ListMembersQuery>,
308    ) -> Result<Json<ListMembersResponse>, AuthError> {
309        let ctx = state.to_context();
310        let result = list_members_core(&query, &user, &session, &ctx).await?;
311        Ok(Json(result))
312    }
313
314    async fn handle_remove_member<DB: DatabaseAdapter>(
315        State(state): State<AuthState<DB>>,
316        Extension(ps): Extension<Arc<PluginState>>,
317        CurrentSession { user, session, .. }: CurrentSession<DB>,
318        Json(body): Json<RemoveMemberRequest>,
319    ) -> Result<Json<RemovedMemberResponse>, AuthError> {
320        let ctx = state.to_context();
321        let result = remove_member_core(&body, &user, &session, &ps.config, &ctx).await?;
322        Ok(Json(result))
323    }
324
325    async fn handle_update_member_role<DB: DatabaseAdapter>(
326        State(state): State<AuthState<DB>>,
327        Extension(ps): Extension<Arc<PluginState>>,
328        CurrentSession { user, session, .. }: CurrentSession<DB>,
329        Json(body): Json<UpdateMemberRoleRequest>,
330    ) -> Result<Json<MemberWrappedResponse>, AuthError> {
331        let ctx = state.to_context();
332        let result = update_member_role_core(&body, &user, &session, &ps.config, &ctx).await?;
333        Ok(Json(result))
334    }
335
336    // -- Invitations --
337
338    async fn handle_invite_member<DB: DatabaseAdapter>(
339        State(state): State<AuthState<DB>>,
340        Extension(ps): Extension<Arc<PluginState>>,
341        CurrentSession { user, session, .. }: CurrentSession<DB>,
342        ValidatedJson(body): ValidatedJson<InviteMemberRequest>,
343    ) -> Result<Json<DB::Invitation>, AuthError> {
344        let ctx = state.to_context();
345        let result = invite_member_core(&body, &user, &session, &ps.config, &ctx).await?;
346        Ok(Json(result))
347    }
348
349    async fn handle_get_invitation<DB: DatabaseAdapter>(
350        State(state): State<AuthState<DB>>,
351        Query(query): Query<GetInvitationQuery>,
352    ) -> Result<Json<GetInvitationResponse<DB::Invitation>>, AuthError> {
353        let ctx = state.to_context();
354        let result = get_invitation_core(&query, &ctx).await?;
355        Ok(Json(result))
356    }
357
358    async fn handle_list_invitations<DB: DatabaseAdapter>(
359        State(state): State<AuthState<DB>>,
360        CurrentSession { user, session, .. }: CurrentSession<DB>,
361        Query(query): Query<ListInvitationsQuery>,
362    ) -> Result<Json<Vec<DB::Invitation>>, AuthError> {
363        let ctx = state.to_context();
364        let result = list_invitations_core(&query, &user, &session, &ctx).await?;
365        Ok(Json(result))
366    }
367
368    async fn handle_list_user_invitations<DB: DatabaseAdapter>(
369        State(state): State<AuthState<DB>>,
370        CurrentSession { user, .. }: CurrentSession<DB>,
371    ) -> Result<Json<Vec<DB::Invitation>>, AuthError> {
372        let ctx = state.to_context();
373        let result = list_user_invitations_core(&user, &ctx).await?;
374        Ok(Json(result))
375    }
376
377    async fn handle_accept_invitation<DB: DatabaseAdapter>(
378        State(state): State<AuthState<DB>>,
379        Extension(ps): Extension<Arc<PluginState>>,
380        CurrentSession { user, session, .. }: CurrentSession<DB>,
381        Json(body): Json<AcceptInvitationRequest>,
382    ) -> Result<Json<AcceptInvitationResponse<DB::Invitation>>, AuthError> {
383        let ctx = state.to_context();
384        let result = accept_invitation_core(&body, &user, &session, &ps.config, &ctx).await?;
385        Ok(Json(result))
386    }
387
388    async fn handle_reject_invitation<DB: DatabaseAdapter>(
389        State(state): State<AuthState<DB>>,
390        CurrentSession { user, .. }: CurrentSession<DB>,
391        Json(body): Json<RejectInvitationRequest>,
392    ) -> Result<Json<SuccessResponse>, AuthError> {
393        let ctx = state.to_context();
394        let result = reject_invitation_core(&body, &user, &ctx).await?;
395        Ok(Json(result))
396    }
397
398    async fn handle_cancel_invitation<DB: DatabaseAdapter>(
399        State(state): State<AuthState<DB>>,
400        Extension(ps): Extension<Arc<PluginState>>,
401        CurrentSession { user, .. }: CurrentSession<DB>,
402        Json(body): Json<CancelInvitationRequest>,
403    ) -> Result<Json<SuccessResponse>, AuthError> {
404        let ctx = state.to_context();
405        let result = cancel_invitation_core(&body, &user, &ps.config, &ctx).await?;
406        Ok(Json(result))
407    }
408
409    // -- Permission check --
410
411    async fn handle_has_permission<DB: DatabaseAdapter>(
412        State(state): State<AuthState<DB>>,
413        Extension(ps): Extension<Arc<PluginState>>,
414        CurrentSession { user, session, .. }: CurrentSession<DB>,
415        Json(body): Json<HasPermissionRequest>,
416    ) -> Result<Json<HasPermissionResponse>, AuthError> {
417        let ctx = state.to_context();
418        let result = has_permission_core(&body, &user, &session, &ps.config, &ctx).await?;
419        Ok(Json(result))
420    }
421
422    // -- AxumPlugin impl --
423
424    #[async_trait]
425    impl<DB: DatabaseAdapter> AxumPlugin<DB> for OrganizationPlugin {
426        fn name(&self) -> &'static str {
427            "organization"
428        }
429
430        fn router(&self) -> axum::Router<AuthState<DB>> {
431            use axum::routing::{get, post};
432
433            let plugin_state = Arc::new(PluginState {
434                config: self.config.clone(),
435            });
436
437            axum::Router::new()
438                // Organization CRUD
439                .route(
440                    "/organization/create",
441                    post(handle_create_organization::<DB>),
442                )
443                .route(
444                    "/organization/update",
445                    post(handle_update_organization::<DB>),
446                )
447                .route(
448                    "/organization/delete",
449                    post(handle_delete_organization::<DB>),
450                )
451                .route("/organization/list", get(handle_list_organizations::<DB>))
452                .route(
453                    "/organization/get-full-organization",
454                    get(handle_get_full_organization::<DB>),
455                )
456                .route("/organization/check-slug", post(handle_check_slug::<DB>))
457                .route(
458                    "/organization/set-active",
459                    post(handle_set_active_organization::<DB>),
460                )
461                .route("/organization/leave", post(handle_leave_organization::<DB>))
462                // Member management
463                .route(
464                    "/organization/get-active-member",
465                    get(handle_get_active_member::<DB>),
466                )
467                .route("/organization/list-members", get(handle_list_members::<DB>))
468                .route(
469                    "/organization/remove-member",
470                    post(handle_remove_member::<DB>),
471                )
472                .route(
473                    "/organization/update-member-role",
474                    post(handle_update_member_role::<DB>),
475                )
476                // Invitations
477                .route(
478                    "/organization/invite-member",
479                    post(handle_invite_member::<DB>),
480                )
481                .route(
482                    "/organization/get-invitation",
483                    get(handle_get_invitation::<DB>),
484                )
485                .route(
486                    "/organization/list-invitations",
487                    get(handle_list_invitations::<DB>),
488                )
489                .route(
490                    "/organization/list-user-invitations",
491                    get(handle_list_user_invitations::<DB>),
492                )
493                .route(
494                    "/organization/accept-invitation",
495                    post(handle_accept_invitation::<DB>),
496                )
497                .route(
498                    "/organization/reject-invitation",
499                    post(handle_reject_invitation::<DB>),
500                )
501                .route(
502                    "/organization/cancel-invitation",
503                    post(handle_cancel_invitation::<DB>),
504                )
505                // Permission check
506                .route(
507                    "/organization/has-permission",
508                    post(handle_has_permission::<DB>),
509                )
510                .layer(Extension(plugin_state))
511        }
512    }
513}