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::error::AuthResult;
9use better_auth_core::plugin::{AuthContext, AuthPlugin, AuthRoute};
10use better_auth_core::types::{AuthRequest, AuthResponse, HttpMethod};
11
12/// Permission definitions for a role
13#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
14pub struct RolePermissions {
15    pub organization: Vec<String>,
16    pub member: Vec<String>,
17    pub invitation: Vec<String>,
18    /// Actions this role may perform on organization-owned API keys. Upstream's
19    /// default statements define none, so only the creator role can manage them
20    /// until an application grants this explicitly.
21    pub api_key: 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/// Metadata key announcing that the organization plugin is installed.
60pub(crate) const METADATA_ENABLED: &str = "organization.enabled";
61/// Metadata key carrying the configured custom roles, so other plugins can run
62/// the organization's access control without depending on this plugin's config.
63pub(crate) const METADATA_ROLES: &str = "organization.roles";
64/// Metadata key carrying the creator role, which is allowed every action.
65pub(crate) const METADATA_CREATOR_ROLE: &str = "organization.creator_role";
66
67#[async_trait]
68impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for OrganizationPlugin {
69    fn name(&self) -> &'static str {
70        "organization"
71    }
72
73    async fn on_init(
74        &self,
75        ctx: &mut better_auth_core::AuthInitContext<S>,
76    ) -> better_auth_core::AuthResult<()> {
77        ctx.set_metadata(METADATA_ENABLED, serde_json::Value::Bool(true));
78        ctx.set_metadata(
79            METADATA_ROLES,
80            serde_json::to_value(&self.config.roles).unwrap_or_default(),
81        );
82        ctx.set_metadata(
83            METADATA_CREATOR_ROLE,
84            serde_json::Value::String(self.config.creator_role.clone()),
85        );
86        Ok(())
87    }
88
89    fn routes(&self) -> Vec<AuthRoute> {
90        vec![
91            // Organization CRUD
92            AuthRoute::post("/organization/create", "create_organization"),
93            AuthRoute::post("/organization/update", "update_organization"),
94            AuthRoute::post("/organization/delete", "delete_organization"),
95            AuthRoute::get("/organization/list", "list_organizations"),
96            AuthRoute::get(
97                "/organization/get-full-organization",
98                "get_full_organization",
99            ),
100            AuthRoute::post("/organization/check-slug", "check_slug"),
101            AuthRoute::post("/organization/set-active", "set_active_organization"),
102            AuthRoute::post("/organization/leave", "leave_organization"),
103            // Member management
104            AuthRoute::get("/organization/get-active-member", "get_active_member"),
105            AuthRoute::get(
106                "/organization/get-active-member-role",
107                "get_active_member_role",
108            ),
109            AuthRoute::get("/organization/list-members", "list_members"),
110            AuthRoute::post("/organization/remove-member", "remove_member"),
111            AuthRoute::post("/organization/update-member-role", "update_member_role"),
112            // Invitations
113            AuthRoute::post("/organization/invite-member", "invite_member"),
114            AuthRoute::get("/organization/get-invitation", "get_invitation"),
115            AuthRoute::get("/organization/list-invitations", "list_invitations"),
116            AuthRoute::get(
117                "/organization/list-user-invitations",
118                "list_user_invitations",
119            ),
120            AuthRoute::post("/organization/accept-invitation", "accept_invitation"),
121            AuthRoute::post("/organization/reject-invitation", "reject_invitation"),
122            AuthRoute::post("/organization/cancel-invitation", "cancel_invitation"),
123            // Permission check
124            AuthRoute::post("/organization/has-permission", "has_permission"),
125        ]
126    }
127
128    async fn on_request(
129        &self,
130        req: &AuthRequest,
131        ctx: &AuthContext<S>,
132    ) -> AuthResult<Option<AuthResponse>> {
133        match (req.method(), req.path()) {
134            // Organization CRUD
135            (HttpMethod::Post, "/organization/create") => Ok(Some(
136                handlers::org::handle_create_organization(req, ctx, &self.config).await?,
137            )),
138            (HttpMethod::Post, "/organization/update") => Ok(Some(
139                handlers::org::handle_update_organization(req, ctx, &self.config).await?,
140            )),
141            (HttpMethod::Post, "/organization/delete") => Ok(Some(
142                handlers::org::handle_delete_organization(req, ctx, &self.config).await?,
143            )),
144            (HttpMethod::Get, "/organization/list") => Ok(Some(
145                handlers::org::handle_list_organizations(req, ctx).await?,
146            )),
147            (HttpMethod::Get, "/organization/get-full-organization") => Ok(Some(
148                handlers::org::handle_get_full_organization(req, ctx, &self.config).await?,
149            )),
150            (HttpMethod::Post, "/organization/check-slug") => {
151                Ok(Some(handlers::org::handle_check_slug(req, ctx).await?))
152            }
153            (HttpMethod::Post, "/organization/set-active") => Ok(Some(
154                handlers::org::handle_set_active_organization(req, ctx).await?,
155            )),
156            (HttpMethod::Post, "/organization/leave") => Ok(Some(
157                handlers::org::handle_leave_organization(req, ctx, &self.config).await?,
158            )),
159            // Member management
160            (HttpMethod::Get, "/organization/get-active-member") => Ok(Some(
161                handlers::member::handle_get_active_member(req, ctx).await?,
162            )),
163            (HttpMethod::Get, "/organization/get-active-member-role") => Ok(Some(
164                handlers::member::handle_get_active_member_role(req, ctx).await?,
165            )),
166            (HttpMethod::Get, "/organization/list-members") => {
167                Ok(Some(handlers::member::handle_list_members(req, ctx).await?))
168            }
169            (HttpMethod::Post, "/organization/remove-member") => Ok(Some(
170                handlers::member::handle_remove_member(req, ctx, &self.config).await?,
171            )),
172            (HttpMethod::Post, "/organization/update-member-role") => Ok(Some(
173                handlers::member::handle_update_member_role(req, ctx, &self.config).await?,
174            )),
175            // Invitations
176            (HttpMethod::Post, "/organization/invite-member") => Ok(Some(
177                handlers::invitation::handle_invite_member(req, ctx, &self.config).await?,
178            )),
179            (HttpMethod::Get, "/organization/get-invitation") => Ok(Some(
180                handlers::invitation::handle_get_invitation(req, ctx).await?,
181            )),
182            (HttpMethod::Get, "/organization/list-invitations") => Ok(Some(
183                handlers::invitation::handle_list_invitations(req, ctx).await?,
184            )),
185            (HttpMethod::Get, "/organization/list-user-invitations") => Ok(Some(
186                handlers::invitation::handle_list_user_invitations(req, ctx).await?,
187            )),
188            (HttpMethod::Post, "/organization/accept-invitation") => Ok(Some(
189                handlers::invitation::handle_accept_invitation(req, ctx, &self.config).await?,
190            )),
191            (HttpMethod::Post, "/organization/reject-invitation") => Ok(Some(
192                handlers::invitation::handle_reject_invitation(req, ctx).await?,
193            )),
194            (HttpMethod::Post, "/organization/cancel-invitation") => Ok(Some(
195                handlers::invitation::handle_cancel_invitation(req, ctx, &self.config).await?,
196            )),
197            // Permission check
198            (HttpMethod::Post, "/organization/has-permission") => Ok(Some(
199                handlers::handle_has_permission(req, ctx, &self.config).await?,
200            )),
201            _ => Ok(None),
202        }
203    }
204}