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#[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#[derive(Debug, Clone, better_auth_core::PluginConfig)]
26#[plugin(name = "OrganizationPlugin")]
27pub struct OrganizationConfig {
28 #[config(default = true)]
30 pub allow_user_to_create_organization: bool,
31 #[config(default = None)]
33 pub organization_limit: Option<usize>,
34 #[config(default = Some(100))]
36 pub membership_limit: Option<usize>,
37 #[config(default = "owner".to_string())]
39 pub creator_role: String,
40 #[config(default = 60 * 60 * 48)]
42 pub invitation_expires_in: u64,
43 #[config(default = Some(100))]
45 pub invitation_limit: Option<usize>,
46 #[config(default = false)]
48 pub disable_organization_deletion: bool,
49 #[config(default = HashMap::new(), skip)]
51 pub roles: HashMap<String, RolePermissions>,
52}
53
54pub 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 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 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 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 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 (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 (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 (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 (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#[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 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 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 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 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 #[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 .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 .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 .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 .route(
507 "/organization/has-permission",
508 post(handle_has_permission::<DB>),
509 )
510 .layer(Extension(plugin_state))
511 }
512 }
513}