organizations 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +178 -7
- data/app/controllers/organizations/memberships_controller.rb +25 -0
- data/config/routes.rb +3 -0
- data/context7.json +4 -0
- data/docs/organizations-invitation-accept-create-account.webp +0 -0
- data/docs/organizations-member-permissions.webp +0 -0
- data/docs/organizations-switcher.webp +0 -0
- data/docs/organizations-team-members.webp +0 -0
- data/lib/generators/organizations/install/templates/initializer.rb +23 -1
- data/lib/organizations/configuration.rb +33 -2
- data/lib/organizations/models/concerns/has_organizations.rb +47 -6
- data/lib/organizations/version.rb +1 -1
- data/lib/organizations/view_helpers.rb +19 -7
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41b5d77cf4271b72a242ed018982a20a62de1aa65be8b44aeb0a1bdca6ca095f
|
|
4
|
+
data.tar.gz: 47da19b07cb78b00d63e3c682389d3b9cce80c10860883af947d25fb75ad4fa1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21cd1f525064c45d471ec8a8e271ddb524aa21843130f2124c0e1b2f903aed5edfb8cbe0a3376f1d699983191eabfdada1304b391244ed65f5e1f7292420d693
|
|
7
|
+
data.tar.gz: f8b6512de051110d180d894a3e23c49f6b16f50b7bb81f0ba8f4d79257f79063ee345f23332d1d923bc3baff0f7feeac56f62f726594a4750a6b7b6522a57acc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.4.3] - 2026-03-19
|
|
2
|
+
|
|
3
|
+
- Added `can_view_billing?` and `can_manage_billing?` view helpers for billing permission checks
|
|
4
|
+
- Refactored `can_manage_organization?` and `can_invite_members?` to use shared permission predicate
|
|
5
|
+
- Fixed `pricing_plans` integration examples to use `current_pricing_plan` (effective plan API)
|
|
6
|
+
- Clarified that billing permissions are authorization checks only, not subscription state indicators
|
|
7
|
+
|
|
8
|
+
## [0.4.2] - 2026-03-19
|
|
9
|
+
|
|
10
|
+
- Added `should_create_personal_organization?` predicate as extension seam for conditional personal org creation
|
|
11
|
+
- DSL `create_personal_organization` setting now accepts procs for dynamic evaluation
|
|
12
|
+
- Added `DELETE /memberships/leave` route for users to leave organizations
|
|
13
|
+
- Updated owner deletion guard message to clarify transfer/delete solution
|
|
14
|
+
- Documentation: added "Pattern 4: Hybrid Onboarding" to README
|
|
15
|
+
|
|
1
16
|
## [0.4.1] - 2026-03-17
|
|
2
17
|
|
|
3
18
|
- Fixed `memberships_count` counter cache writes on `Organizations::Membership.create!`
|
data/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
`organizations` adds organizations with members to any Rails app. It handles team invites, user memberships, roles, and permissions.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<img src="docs/organizations-invitation-accept-create-account.webp" width="500" />
|
|
11
11
|
|
|
12
|
-
[
|
|
12
|
+
**🎮 [Try the live demo →](https://organizations.rameerez.com)**
|
|
13
13
|
|
|
14
14
|
It's everything you need to turn a `User`-based app into a multi-tenant, `Organization`-based B2B SaaS (users belong in organizations, and organizations share resources and billing, etc.)
|
|
15
15
|
|
|
@@ -41,6 +41,8 @@ current_user.is_organization_owner? # => true
|
|
|
41
41
|
current_user.is_organization_admin? # => true (owners inherit admin permissions)
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
https://github.com/user-attachments/assets/2eddafe2-025b-4670-af9f-e0d5480508c5
|
|
45
|
+
|
|
44
46
|
## Installation
|
|
45
47
|
|
|
46
48
|
Add to your Gemfile:
|
|
@@ -74,13 +76,13 @@ That's the simplest setup. You can also configure per-model options:
|
|
|
74
76
|
class User < ApplicationRecord
|
|
75
77
|
has_organizations do
|
|
76
78
|
max_organizations 5 # Limit how many orgs a user can own (nil = unlimited)
|
|
77
|
-
create_personal_org true # Auto-create org on signup (default: false)
|
|
79
|
+
create_personal_org true # Auto-create org on signup (default: false). Can also be a proc.
|
|
78
80
|
require_organization true # Require users to have at least one org (default: false)
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
> **Note:** By default, users can exist without any organization (invite-to-join flow). Set `create_personal_org true` if you want to auto-create a personal organization when users sign up.
|
|
85
|
+
> **Note:** By default, users can exist without any organization (invite-to-join flow). Set `create_personal_org true` if you want to auto-create a personal organization when users sign up. For conditional creation, use a proc: `create_personal_org ->(user) { ... }` or override `should_create_personal_organization?` on your User model (see Pattern 4: Hybrid Onboarding).
|
|
84
86
|
|
|
85
87
|
Mount the engine in your routes:
|
|
86
88
|
|
|
@@ -108,6 +110,8 @@ org = current_user.create_organization!("Acme Corp")
|
|
|
108
110
|
|
|
109
111
|
### Invite teammates
|
|
110
112
|
|
|
113
|
+
<img src="docs/organizations-team-members.webp" width="500" />
|
|
114
|
+
|
|
111
115
|
```ruby
|
|
112
116
|
current_user.send_organization_invite_to!("teammate@example.com")
|
|
113
117
|
# Sends invitation email: "John invited you to join Acme Corp"
|
|
@@ -122,6 +126,8 @@ current_user.send_organization_invite_to!("teammate@example.com", organization:
|
|
|
122
126
|
|
|
123
127
|
### Check roles and permissions
|
|
124
128
|
|
|
129
|
+
<img src="docs/organizations-member-permissions.webp" width="500" />
|
|
130
|
+
|
|
125
131
|
```ruby
|
|
126
132
|
# Quick role checks (in current organization)
|
|
127
133
|
current_user.is_organization_owner? # => true/false
|
|
@@ -141,6 +147,8 @@ current_user.is_member_of?(@org)
|
|
|
141
147
|
|
|
142
148
|
### Switch between organizations
|
|
143
149
|
|
|
150
|
+
<img src="docs/organizations-switcher.webp" width="400" />
|
|
151
|
+
|
|
144
152
|
```ruby
|
|
145
153
|
# User belongs to multiple organizations? No problem.
|
|
146
154
|
current_user.organizations # => [acme, startup_co, personal]
|
|
@@ -160,7 +168,7 @@ end
|
|
|
160
168
|
|
|
161
169
|
> **Note:** This is an integration pattern, not built-in functionality. You implement the limit checks in your callbacks.
|
|
162
170
|
|
|
163
|
-
If you're using [`pricing_plans`](https://github.com/rameerez/pricing_plans), you can limit how many members an organization can have based on their
|
|
171
|
+
If you're using [`pricing_plans`](https://github.com/rameerez/pricing_plans), you can limit how many members an organization can have based on their effective pricing plan using callbacks:
|
|
164
172
|
|
|
165
173
|
```ruby
|
|
166
174
|
# config/initializers/pricing_plans.rb
|
|
@@ -180,7 +188,7 @@ Then hook into the `on_member_invited` callback to enforce limits. **This callba
|
|
|
180
188
|
Organizations.configure do |config|
|
|
181
189
|
config.on_member_invited do |ctx|
|
|
182
190
|
org = ctx.organization
|
|
183
|
-
limit = org.
|
|
191
|
+
limit = org.current_pricing_plan.limit_for(:organization_members)
|
|
184
192
|
|
|
185
193
|
if limit && org.member_count >= limit
|
|
186
194
|
raise Organizations::InvitationError, "Member limit reached. Please upgrade your plan."
|
|
@@ -428,6 +436,8 @@ class SettingsController < ApplicationController
|
|
|
428
436
|
end
|
|
429
437
|
```
|
|
430
438
|
|
|
439
|
+
`manage_billing` and `view_billing` are authorization checks only. They control who in the organization can access your billing UI, but they do not imply an active Stripe subscription or determine the effective pricing plan.
|
|
440
|
+
|
|
431
441
|
### Handling unauthorized access
|
|
432
442
|
|
|
433
443
|
Configure how unauthorized access is handled:
|
|
@@ -652,6 +662,8 @@ org.send_invite_to!("teammate@example.com", invited_by: current_user)
|
|
|
652
662
|
|
|
653
663
|
The gem handles **both existing users and new signups** with a single invitation link:
|
|
654
664
|
|
|
665
|
+
<img src="docs/organizations-invitation-accept-create-account.webp" width="500" />
|
|
666
|
+
|
|
655
667
|
**For existing users:**
|
|
656
668
|
1. Invitation created → Email sent with unique link
|
|
657
669
|
2. User clicks link → Sees invitation details (org name, inviter, role)
|
|
@@ -888,6 +900,165 @@ Organizations.configure do |config|
|
|
|
888
900
|
end
|
|
889
901
|
```
|
|
890
902
|
|
|
903
|
+
## User Onboarding Patterns
|
|
904
|
+
|
|
905
|
+
The `always_create_personal_organization_for_each_user` setting controls how new users get their first organization. This is one of the most important decisions when integrating the gem.
|
|
906
|
+
|
|
907
|
+
### Pattern 1: Instant Access (auto-create)
|
|
908
|
+
|
|
909
|
+
**Think:** Notion, Slack, Trello — "Sign up and start using it in 10 seconds"
|
|
910
|
+
|
|
911
|
+
```ruby
|
|
912
|
+
config.always_create_personal_organization_for_each_user = true
|
|
913
|
+
config.default_organization_name = "My Workspace"
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
```
|
|
917
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
918
|
+
│ User signs up → "My Workspace" created → Dashboard 🎉 │
|
|
919
|
+
└─────────────────────────────────────────────────────────────┘
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
Users land in the app immediately. Zero friction. They can invite teammates later.
|
|
923
|
+
|
|
924
|
+
```ruby
|
|
925
|
+
# config/initializers/organizations.rb
|
|
926
|
+
Organizations.configure do |config|
|
|
927
|
+
config.always_create_personal_organization_for_each_user = true
|
|
928
|
+
config.default_organization_name = "My Workspace"
|
|
929
|
+
# Or personalize it:
|
|
930
|
+
# config.default_organization_name = ->(user) { "#{user.email.split('@').first}'s Workspace" }
|
|
931
|
+
end
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
Best for: productivity tools, note apps, personal SaaS, anything where "just let me in" matters.
|
|
935
|
+
|
|
936
|
+
### Pattern 2: Guided Onboarding (manual create)
|
|
937
|
+
|
|
938
|
+
**Think:** Stripe, HubSpot, Salesforce — "Tell us about your company first"
|
|
939
|
+
|
|
940
|
+
```ruby
|
|
941
|
+
config.always_create_personal_organization_for_each_user = false
|
|
942
|
+
config.redirect_path_when_no_organization = "/onboarding/create_organization"
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
```
|
|
946
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
947
|
+
│ User signs up → Onboarding wizard → "Create your company" → App │
|
|
948
|
+
│ (collect company name, billing email, etc.) │
|
|
949
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
You control the experience. Collect whatever info you need before they enter.
|
|
953
|
+
|
|
954
|
+
```ruby
|
|
955
|
+
# config/initializers/organizations.rb
|
|
956
|
+
Organizations.configure do |config|
|
|
957
|
+
config.always_create_personal_organization_for_each_user = false
|
|
958
|
+
config.redirect_path_when_no_organization = "/onboarding/create_organization"
|
|
959
|
+
config.no_organization_notice = "Let's set up your company first."
|
|
960
|
+
config.additional_organization_params = [:billing_email, :company_size, :industry]
|
|
961
|
+
end
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
```ruby
|
|
965
|
+
# app/controllers/onboarding_controller.rb
|
|
966
|
+
def create_organization
|
|
967
|
+
@organization = current_user.create_organization!(
|
|
968
|
+
name: params[:company_name],
|
|
969
|
+
billing_email: params[:billing_email]
|
|
970
|
+
)
|
|
971
|
+
redirect_to dashboard_path
|
|
972
|
+
end
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Best for: B2B SaaS, enterprise tools, apps that need company details for billing/compliance.
|
|
976
|
+
|
|
977
|
+
### Pattern 3: Invitation-Only
|
|
978
|
+
|
|
979
|
+
**Think:** Internal company tools, private beta, enterprise deployments
|
|
980
|
+
|
|
981
|
+
```ruby
|
|
982
|
+
config.always_create_personal_organization_for_each_user = false
|
|
983
|
+
config.redirect_path_when_no_organization = "/waiting_room"
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
```
|
|
987
|
+
┌───────────────────────────────────────────────────────────────────────┐
|
|
988
|
+
│ User signs up → "Waiting for invitation" page → (nothing yet) │
|
|
989
|
+
│ │
|
|
990
|
+
│ Admin invites user → User accepts → Joins org → Dashboard 🎉 │
|
|
991
|
+
└───────────────────────────────────────────────────────────────────────┘
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
Users can't do anything until an admin invites them. Full control over who gets in.
|
|
995
|
+
|
|
996
|
+
Best for: internal tools, private beta programs, enterprise B2B where orgs are pre-provisioned.
|
|
997
|
+
|
|
998
|
+
### Pattern 4: Hybrid Onboarding
|
|
999
|
+
|
|
1000
|
+
**Think:** Best of both worlds — instant access for direct signups, no clutter for invited users
|
|
1001
|
+
|
|
1002
|
+
Direct signups get a personal workspace immediately. Invited users skip the personal workspace and join the organization that invited them directly. This avoids creating unnecessary "My Workspace" organizations for users who are joining an existing team.
|
|
1003
|
+
|
|
1004
|
+
```
|
|
1005
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
1006
|
+
│ Direct signup → "My Workspace" auto-created → Dashboard 🎉 │
|
|
1007
|
+
│ │
|
|
1008
|
+
│ Invited signup → No personal org → Joins invited org → Dashboard 🎉 │
|
|
1009
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Implementation requires overriding the `should_create_personal_organization?` method on your User model:
|
|
1013
|
+
|
|
1014
|
+
```ruby
|
|
1015
|
+
# app/models/user.rb
|
|
1016
|
+
class User < ApplicationRecord
|
|
1017
|
+
has_organizations
|
|
1018
|
+
|
|
1019
|
+
# Skip personal org creation for users signing up via invitation
|
|
1020
|
+
attr_accessor :skip_personal_organization
|
|
1021
|
+
|
|
1022
|
+
def should_create_personal_organization?
|
|
1023
|
+
return false if skip_personal_organization
|
|
1024
|
+
super
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
Then set the flag before the user is persisted. With Devise, override `build_resource`:
|
|
1030
|
+
|
|
1031
|
+
```ruby
|
|
1032
|
+
# app/controllers/users/registrations_controller.rb
|
|
1033
|
+
class Users::RegistrationsController < Devise::RegistrationsController
|
|
1034
|
+
protected
|
|
1035
|
+
|
|
1036
|
+
def build_resource(hash = {})
|
|
1037
|
+
super
|
|
1038
|
+
# Skip personal org for users signing up via invitation
|
|
1039
|
+
resource.skip_personal_organization = true if pending_organization_invitation?
|
|
1040
|
+
end
|
|
1041
|
+
end
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
The `should_create_personal_organization?` method is the official extension seam. It evaluates:
|
|
1045
|
+
1. Your method override (if defined)
|
|
1046
|
+
2. The `create_personal_org` setting (boolean or proc)
|
|
1047
|
+
3. The global `always_create_personal_organization_for_each_user` config
|
|
1048
|
+
|
|
1049
|
+
You can also use a proc for the setting itself:
|
|
1050
|
+
|
|
1051
|
+
```ruby
|
|
1052
|
+
# In has_organizations block
|
|
1053
|
+
has_organizations do
|
|
1054
|
+
create_personal_org ->(user) { !user.skip_personal_organization }
|
|
1055
|
+
end
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
Best for: SaaS products that want instant onboarding for individual users but clean team onboarding for invited members.
|
|
1059
|
+
|
|
1060
|
+
---
|
|
1061
|
+
|
|
891
1062
|
## Configuration
|
|
892
1063
|
|
|
893
1064
|
Full configuration options:
|
|
@@ -1048,7 +1219,7 @@ end
|
|
|
1048
1219
|
|
|
1049
1220
|
### Integrates with pricing_plans
|
|
1050
1221
|
|
|
1051
|
-
Enforce member limits based on pricing
|
|
1222
|
+
Enforce member limits based on the effective pricing plan using callbacks:
|
|
1052
1223
|
|
|
1053
1224
|
```ruby
|
|
1054
1225
|
# In your Organization model
|
|
@@ -79,6 +79,31 @@ module Organizations
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# DELETE /memberships/leave
|
|
83
|
+
# Leave the current organization (current user removes themselves)
|
|
84
|
+
# Note: No permission guard needed — users can only leave themselves, and
|
|
85
|
+
# require_organization! ensures valid org context. Domain rules (can't leave
|
|
86
|
+
# as last owner, etc.) are enforced by leave_organization!.
|
|
87
|
+
def leave
|
|
88
|
+
org_name = current_organization.name
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
current_user.leave_organization!(current_organization)
|
|
92
|
+
|
|
93
|
+
respond_to do |format|
|
|
94
|
+
format.html { redirect_to organizations_path, notice: "You have left #{org_name}." }
|
|
95
|
+
format.json { head :no_content }
|
|
96
|
+
end
|
|
97
|
+
rescue Models::Concerns::HasOrganizations::CannotLeaveLastOrganization,
|
|
98
|
+
Models::Concerns::HasOrganizations::CannotLeaveAsLastOwner,
|
|
99
|
+
Error => e
|
|
100
|
+
respond_to do |format|
|
|
101
|
+
format.html { redirect_back fallback_location: organizations_path, alert: e.message }
|
|
102
|
+
format.json { render json: { error: e.message }, status: :unprocessable_entity }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
82
107
|
# POST /memberships/:id/transfer_ownership
|
|
83
108
|
# Transfer organization ownership to another member
|
|
84
109
|
def transfer_ownership
|
data/config/routes.rb
CHANGED
data/context7.json
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,11 +6,33 @@ Organizations.configure do |config|
|
|
|
6
6
|
# ============================================================================
|
|
7
7
|
|
|
8
8
|
# Automatically create a personal organization when a user signs up.
|
|
9
|
-
# The organization will be named
|
|
9
|
+
# The organization will be named using default_organization_name (below).
|
|
10
10
|
# Set to true if you want every user to have their own organization on signup.
|
|
11
11
|
# Default: false (invite-to-join flow)
|
|
12
12
|
# config.always_create_personal_organization_for_each_user = false
|
|
13
13
|
|
|
14
|
+
# NOTE: This is a coarse, global default. For conditional creation (e.g., skip
|
|
15
|
+
# personal org for invited signups), override should_create_personal_organization?
|
|
16
|
+
# on your User model instead:
|
|
17
|
+
#
|
|
18
|
+
# class User < ApplicationRecord
|
|
19
|
+
# has_organizations
|
|
20
|
+
# attr_accessor :skip_personal_organization
|
|
21
|
+
#
|
|
22
|
+
# def should_create_personal_organization?
|
|
23
|
+
# return false if skip_personal_organization
|
|
24
|
+
# super
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# See "Pattern 4: Hybrid Onboarding" in the README for the full pattern.
|
|
29
|
+
|
|
30
|
+
# Name for auto-created organizations.
|
|
31
|
+
# Can be a string or a proc that receives the user.
|
|
32
|
+
# Default: "Personal"
|
|
33
|
+
# config.default_organization_name = "My Workspace"
|
|
34
|
+
# config.default_organization_name = ->(user) { "#{user.name}'s Workspace" }
|
|
35
|
+
|
|
14
36
|
# ============================================================================
|
|
15
37
|
# ORGANIZATION REQUIREMENTS
|
|
16
38
|
# ============================================================================
|
|
@@ -33,10 +33,41 @@ module Organizations
|
|
|
33
33
|
attr_accessor :authenticate_user_method
|
|
34
34
|
|
|
35
35
|
# === Auto-creation ===
|
|
36
|
-
#
|
|
36
|
+
#
|
|
37
|
+
# Create personal organization automatically on user signup.
|
|
38
|
+
#
|
|
39
|
+
# This setting controls the user's first-time experience:
|
|
40
|
+
#
|
|
41
|
+
# ┌─────────────────────────────────────────────────────────────────────────┐
|
|
42
|
+
# │ true → "Instant access" pattern │
|
|
43
|
+
# │ │
|
|
44
|
+
# │ User signs up → auto-created workspace → lands in app immediately │
|
|
45
|
+
# │ │
|
|
46
|
+
# │ Think: Notion, Slack, Trello │
|
|
47
|
+
# │ "Sign up and start using it in seconds" │
|
|
48
|
+
# │ │
|
|
49
|
+
# │ Best for: productivity tools, note apps, simple SaaS │
|
|
50
|
+
# └─────────────────────────────────────────────────────────────────────────┘
|
|
51
|
+
#
|
|
52
|
+
# ┌─────────────────────────────────────────────────────────────────────────┐
|
|
53
|
+
# │ false → "Guided onboarding" pattern │
|
|
54
|
+
# │ │
|
|
55
|
+
# │ User signs up → onboarding wizard → enters company info → dashboard │
|
|
56
|
+
# │ │
|
|
57
|
+
# │ Think: Stripe, HubSpot, enterprise B2B tools │
|
|
58
|
+
# │ "Tell us about your company before you start" │
|
|
59
|
+
# │ │
|
|
60
|
+
# │ Best for: B2B SaaS needing company details, billing info, etc. │
|
|
61
|
+
# └─────────────────────────────────────────────────────────────────────────┘
|
|
62
|
+
#
|
|
63
|
+
# Related settings:
|
|
64
|
+
# - default_organization_name: Name for auto-created orgs (only when true)
|
|
65
|
+
# - redirect_path_when_no_organization: Where to send users without an org
|
|
66
|
+
# - always_require_users_to_belong_to_one_organization: Prevent leaving last org
|
|
67
|
+
#
|
|
37
68
|
attr_accessor :always_create_personal_organization_for_each_user
|
|
38
69
|
|
|
39
|
-
# Name for auto-created organizations
|
|
70
|
+
# Name for auto-created organizations (only used when always_create_personal_organization_for_each_user = true)
|
|
40
71
|
# Can be a String or a Proc/Lambda: ->(user) { "#{user.name}'s Workspace" }
|
|
41
72
|
attr_accessor :default_organization_name
|
|
42
73
|
|
|
@@ -37,7 +37,8 @@ module Organizations
|
|
|
37
37
|
# Enable organization support on this model
|
|
38
38
|
# @param options [Hash] Configuration options
|
|
39
39
|
# @option options [Integer, nil] :max_organizations Maximum orgs user can own
|
|
40
|
-
# @option options [Boolean] :create_personal_org Create personal org on signup
|
|
40
|
+
# @option options [Boolean, Proc] :create_personal_org Create personal org on signup.
|
|
41
|
+
# Can be a boolean or a proc that receives the user instance for conditional creation.
|
|
41
42
|
# @option options [Boolean] :require_organization Require user to have an org
|
|
42
43
|
# @yield Configuration block using DSL
|
|
43
44
|
def has_organizations(**options, &block)
|
|
@@ -111,9 +112,10 @@ module Organizations
|
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
def setup_personal_org_callback
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
# Callback that creates personal organization.
|
|
116
|
+
# The condition is checked inside the callback to ensure it's evaluated
|
|
117
|
+
# at the right time with the correct instance state.
|
|
118
|
+
after_create :create_personal_organization_if_configured
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
def setup_owner_deletion_guard
|
|
@@ -136,7 +138,14 @@ module Organizations
|
|
|
136
138
|
end
|
|
137
139
|
|
|
138
140
|
# Enable/disable personal organization creation on signup
|
|
139
|
-
# @param value [Boolean]
|
|
141
|
+
# @param value [Boolean, Proc] Boolean or proc that receives the user instance
|
|
142
|
+
#
|
|
143
|
+
# @example Boolean (backward-compatible)
|
|
144
|
+
# create_personal_org true
|
|
145
|
+
#
|
|
146
|
+
# @example Proc for conditional creation
|
|
147
|
+
# create_personal_org ->(user) { !user.skip_personal_organization }
|
|
148
|
+
#
|
|
140
149
|
def create_personal_org(value)
|
|
141
150
|
@settings[:create_personal_org] = value
|
|
142
151
|
end
|
|
@@ -499,9 +508,41 @@ module Organizations
|
|
|
499
508
|
org.send_invite_to!(email, invited_by: self, role: role)
|
|
500
509
|
end
|
|
501
510
|
|
|
511
|
+
# Extension seam for personal organization creation.
|
|
512
|
+
# Override this method in your User model to implement custom logic.
|
|
513
|
+
#
|
|
514
|
+
# @return [Boolean] true if a personal organization should be created
|
|
515
|
+
#
|
|
516
|
+
# @example Skip personal org for invited signups
|
|
517
|
+
# class User < ApplicationRecord
|
|
518
|
+
# has_organizations
|
|
519
|
+
# attr_accessor :skip_personal_organization
|
|
520
|
+
#
|
|
521
|
+
# def should_create_personal_organization?
|
|
522
|
+
# return false if skip_personal_organization
|
|
523
|
+
# super
|
|
524
|
+
# end
|
|
525
|
+
# end
|
|
526
|
+
#
|
|
527
|
+
def should_create_personal_organization?
|
|
528
|
+
setting = self.class.organization_settings[:create_personal_org]
|
|
529
|
+
|
|
530
|
+
case setting
|
|
531
|
+
when Proc
|
|
532
|
+
setting.call(self)
|
|
533
|
+
else
|
|
534
|
+
!!setting
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
502
538
|
private
|
|
503
539
|
|
|
504
540
|
def create_personal_organization_if_configured
|
|
541
|
+
# Check condition inside callback (not via `if:` option) to ensure
|
|
542
|
+
# proper evaluation of instance state, including singleton methods
|
|
543
|
+
# and dynamically-set attributes.
|
|
544
|
+
return unless should_create_personal_organization?
|
|
545
|
+
|
|
505
546
|
org_name = Organizations.configuration.resolve_default_organization_name(self)
|
|
506
547
|
create_organization!(org_name)
|
|
507
548
|
rescue StandardError => e
|
|
@@ -512,7 +553,7 @@ module Organizations
|
|
|
512
553
|
def prevent_deletion_while_owning_organizations
|
|
513
554
|
return unless memberships.where(role: "owner").exists?
|
|
514
555
|
|
|
515
|
-
errors.add(:base, "Cannot delete a user who still owns organizations. Transfer ownership first.")
|
|
556
|
+
errors.add(:base, "Cannot delete a user who still owns organizations. Transfer ownership or delete those organizations first.")
|
|
516
557
|
throw(:abort)
|
|
517
558
|
end
|
|
518
559
|
end
|
|
@@ -213,10 +213,7 @@ module Organizations
|
|
|
213
213
|
# @param organization [Organizations::Organization] The organization
|
|
214
214
|
# @return [Boolean]
|
|
215
215
|
def can_manage_organization?(user, organization)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
role = user.role_in(organization)
|
|
219
|
-
role && Roles.has_permission?(role, :manage_settings)
|
|
216
|
+
user_has_permission_in_org?(user, organization, :manage_settings)
|
|
220
217
|
end
|
|
221
218
|
|
|
222
219
|
# Check if current user can invite members
|
|
@@ -225,10 +222,25 @@ module Organizations
|
|
|
225
222
|
# @param organization [Organizations::Organization] The organization
|
|
226
223
|
# @return [Boolean]
|
|
227
224
|
def can_invite_members?(user, organization)
|
|
228
|
-
|
|
225
|
+
user_has_permission_in_org?(user, organization, :invite_members)
|
|
226
|
+
end
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
# Check if current user can view billing information
|
|
229
|
+
# Uses permission-based check to respect custom role configurations
|
|
230
|
+
# @param user [User] The user
|
|
231
|
+
# @param organization [Organizations::Organization] The organization
|
|
232
|
+
# @return [Boolean]
|
|
233
|
+
def can_view_billing?(user, organization)
|
|
234
|
+
user_has_permission_in_org?(user, organization, :view_billing)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if current user can manage billing
|
|
238
|
+
# Uses permission-based check to respect custom role configurations
|
|
239
|
+
# @param user [User] The user
|
|
240
|
+
# @param organization [Organizations::Organization] The organization
|
|
241
|
+
# @return [Boolean]
|
|
242
|
+
def can_manage_billing?(user, organization)
|
|
243
|
+
user_has_permission_in_org?(user, organization, :manage_billing)
|
|
232
244
|
end
|
|
233
245
|
|
|
234
246
|
# Check if current user can remove a member
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: organizations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-03-
|
|
10
|
+
date: 2026-03-19 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: railties
|
|
@@ -103,6 +103,11 @@ files:
|
|
|
103
103
|
- app/views/organizations/invitation_mailer/invitation_email.html.erb
|
|
104
104
|
- app/views/organizations/invitation_mailer/invitation_email.text.erb
|
|
105
105
|
- config/routes.rb
|
|
106
|
+
- context7.json
|
|
107
|
+
- docs/organizations-invitation-accept-create-account.webp
|
|
108
|
+
- docs/organizations-member-permissions.webp
|
|
109
|
+
- docs/organizations-switcher.webp
|
|
110
|
+
- docs/organizations-team-members.webp
|
|
106
111
|
- gemfiles/rails_7.2.gemfile
|
|
107
112
|
- gemfiles/rails_8.1.gemfile
|
|
108
113
|
- lib/generators/organizations/install/install_generator.rb
|