plutonium 0.39.1 → 0.40.0
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/.claude/skills/plutonium-connect-resource/SKILL.md +19 -1
- data/.claude/skills/plutonium-controller/SKILL.md +5 -9
- data/.claude/skills/plutonium-definition-query/SKILL.md +10 -2
- data/.claude/skills/plutonium-installation/SKILL.md +9 -7
- data/.claude/skills/plutonium-invites/SKILL.md +363 -0
- data/.claude/skills/plutonium-package/SKILL.md +2 -1
- data/.claude/skills/plutonium-portal/SKILL.md +30 -16
- data/.claude/skills/plutonium-rodauth/SKILL.md +111 -18
- data/CHANGELOG.md +48 -0
- data/app/assets/plutonium.css +1 -1
- data/config/initializers/sqlite_alias.rb +8 -8
- data/docs/.vitepress/config.ts +1 -0
- data/docs/getting-started/tutorial/07-author-portal.md +1 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +5 -2
- data/docs/guides/adding-resources.md +10 -0
- data/docs/guides/authentication.md +15 -8
- data/docs/guides/creating-packages.md +13 -8
- data/docs/guides/index.md +2 -0
- data/docs/guides/search-filtering.md +8 -3
- data/docs/guides/user-invites.md +497 -0
- data/docs/public/templates/base.rb +5 -1
- data/docs/public/templates/lite.rb +42 -0
- data/docs/public/templates/pluton8.rb +7 -2
- data/docs/reference/controller/index.md +12 -7
- data/docs/reference/definition/query.md +12 -3
- data/docs/reference/generators/index.md +70 -10
- data/docs/reference/portal/index.md +22 -11
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +31 -0
- data/lib/generators/pu/gem/active_shrine/templates/config/initializers/shrine.rb.tt +58 -0
- data/lib/generators/pu/gem/annotated/templates/lib/tasks/auto_annotate_models.rake +6 -1
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -0
- data/lib/generators/pu/invites/USAGE +27 -0
- data/lib/generators/pu/invites/install_generator.rb +364 -0
- data/lib/generators/pu/invites/invitable/USAGE +31 -0
- data/lib/generators/pu/invites/invitable_generator.rb +143 -0
- data/lib/generators/pu/invites/templates/INSTRUCTIONS +22 -0
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +24 -0
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +26 -0
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +47 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +77 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +68 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/resend_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +34 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +41 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/error.html.erb.tt +24 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +40 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +39 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +49 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +33 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +23 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +130 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +72 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -2
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +7 -1
- data/lib/generators/pu/lite/litestream/litestream_generator.rb +105 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +88 -0
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +14 -0
- data/lib/generators/pu/lite/setup/setup_generator.rb +54 -0
- data/lib/generators/pu/lite/solid_cable/solid_cable_generator.rb +65 -0
- data/lib/generators/pu/lite/solid_cache/solid_cache_generator.rb +66 -0
- data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +61 -0
- data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +107 -0
- data/lib/generators/pu/pkg/portal/USAGE +8 -2
- data/lib/generators/pu/pkg/portal/portal_generator.rb +11 -1
- data/lib/generators/pu/pkg/portal/templates/app/controllers/concerns/controller.rb.tt +2 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +1 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +7 -0
- data/lib/generators/pu/pkg/portal/templates/lib/engine.rb.tt +3 -0
- data/lib/generators/pu/res/conn/USAGE +5 -0
- data/lib/generators/pu/res/conn/conn_generator.rb +30 -4
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +6 -3
- data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +6 -6
- data/lib/generators/pu/rodauth/account_generator.rb +36 -11
- data/lib/generators/pu/rodauth/admin_generator.rb +55 -0
- data/lib/generators/pu/rodauth/install_generator.rb +1 -8
- data/lib/generators/pu/rodauth/templates/app/interactions/invite_admin_interaction.rb.tt +25 -0
- data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +6 -2
- data/lib/generators/pu/saas/USAGE +22 -0
- data/lib/generators/pu/saas/entity/USAGE +19 -0
- data/lib/generators/pu/saas/entity_generator.rb +55 -0
- data/lib/generators/pu/saas/membership/USAGE +25 -0
- data/lib/generators/pu/saas/membership_generator.rb +165 -0
- data/lib/generators/pu/saas/setup/USAGE +27 -0
- data/lib/generators/pu/saas/setup_generator.rb +98 -0
- data/lib/generators/pu/saas/user/USAGE +21 -0
- data/lib/generators/pu/saas/user_generator.rb +66 -0
- data/lib/plutonium/core/controller.rb +9 -5
- data/lib/plutonium/definition/base.rb +3 -1
- data/lib/plutonium/definition/scoping.rb +20 -0
- data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
- data/lib/plutonium/invites/concerns/invitable.rb +98 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
- data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
- data/lib/plutonium/invites/controller.rb +226 -0
- data/lib/plutonium/invites/pending_invite_check.rb +76 -0
- data/lib/plutonium/invites.rb +6 -0
- data/lib/plutonium/resource/controllers/queryable.rb +4 -0
- data/lib/plutonium/resource/query_object.rb +3 -5
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +64 -7
- data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
- data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
- data/public/plutonium-assets/plutonium-logo-original.png +0 -0
- data/public/plutonium-assets/plutonium-logo-white.png +0 -0
- data/public/plutonium-assets/plutonium-logo.png +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
SaaS generators for multi-tenant applications with user accounts, entities, and memberships.
|
|
3
|
+
|
|
4
|
+
Generators:
|
|
5
|
+
pu:saas:setup Complete SaaS setup (user + entity + membership)
|
|
6
|
+
pu:saas:user SaaS user account with Rodauth
|
|
7
|
+
pu:saas:entity Entity/organization model
|
|
8
|
+
pu:saas:membership Membership join table linking users to entities
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
rails g pu:saas:setup --user Customer --entity Organization
|
|
12
|
+
|
|
13
|
+
This creates:
|
|
14
|
+
- Customer user account with Rodauth authentication
|
|
15
|
+
- Organization entity model with unique name
|
|
16
|
+
- OrganizationCustomer membership model with role enum
|
|
17
|
+
|
|
18
|
+
See individual generator help for more options:
|
|
19
|
+
rails g pu:saas:setup --help
|
|
20
|
+
rails g pu:saas:user --help
|
|
21
|
+
rails g pu:saas:entity --help
|
|
22
|
+
rails g pu:saas:membership --help
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate a SaaS entity model (organization/tenant).
|
|
3
|
+
Creates an entity with a unique name field for multi-tenant applications.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
rails g pu:saas:entity Organization
|
|
7
|
+
rails g pu:saas:entity Organization --dest=main_app
|
|
8
|
+
rails g pu:saas:entity Company --extra-attributes=slug:string,domain:string
|
|
9
|
+
|
|
10
|
+
This creates:
|
|
11
|
+
app/models/organization.rb
|
|
12
|
+
app/controllers/organizations_controller.rb
|
|
13
|
+
app/policies/organization_policy.rb
|
|
14
|
+
app/definitions/organization_definition.rb
|
|
15
|
+
db/migrate/XXX_create_organizations.rb (with unique index on name)
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--extra-attributes Additional model attributes (name:string is always included)
|
|
19
|
+
--dest Destination package (default: main_app)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
require_relative "../lib/plutonium_generators"
|
|
5
|
+
|
|
6
|
+
module Pu
|
|
7
|
+
module Saas
|
|
8
|
+
class EntityGenerator < ::Rails::Generators::NamedBase
|
|
9
|
+
include PlutoniumGenerators::Generator
|
|
10
|
+
|
|
11
|
+
desc "Generate a SaaS entity model (organization/tenant)"
|
|
12
|
+
|
|
13
|
+
class_option :extra_attributes, type: :array, default: [],
|
|
14
|
+
desc: "Additional attributes for the entity model"
|
|
15
|
+
|
|
16
|
+
def start
|
|
17
|
+
generate_entity_resource
|
|
18
|
+
add_unique_index_to_migration
|
|
19
|
+
rescue => e
|
|
20
|
+
exception "#{self.class} failed:", e
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def generate_entity_resource
|
|
26
|
+
invoke "pu:res:scaffold", [normalized_name, *entity_attributes],
|
|
27
|
+
dest: selected_destination_feature,
|
|
28
|
+
model: true,
|
|
29
|
+
force: options[:force],
|
|
30
|
+
skip: options[:skip]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_unique_index_to_migration
|
|
34
|
+
migration_dir = File.join("db", "migrate")
|
|
35
|
+
migration_file = Dir[File.join(migration_dir, "*_create_#{normalized_name.pluralize}.rb")].first
|
|
36
|
+
|
|
37
|
+
return unless migration_file && File.exist?(migration_file)
|
|
38
|
+
|
|
39
|
+
insert_into_file migration_file,
|
|
40
|
+
indent("add_index :#{normalized_name.pluralize}, :name, unique: true\n", 4),
|
|
41
|
+
before: /^ end\s*$/
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def entity_attributes
|
|
45
|
+
["name:string", *Array(options[:extra_attributes])]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def normalized_name = name.underscore
|
|
49
|
+
|
|
50
|
+
def selected_destination_feature
|
|
51
|
+
feature_option :dest, prompt: "Select destination feature"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate a SaaS membership model linking users to entities.
|
|
3
|
+
Creates a join table with role enum and bidirectional associations.
|
|
4
|
+
|
|
5
|
+
Requires both user and entity models to exist first.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
rails g pu:saas:membership --user Customer --entity Organization
|
|
9
|
+
rails g pu:saas:membership --user Customer --entity Organization --roles=member,admin,owner
|
|
10
|
+
rails g pu:saas:membership --user Customer --entity Organization --extra-attributes=joined_at:datetime
|
|
11
|
+
|
|
12
|
+
This creates:
|
|
13
|
+
app/models/organization_customer.rb (with role enum and uniqueness validation)
|
|
14
|
+
db/migrate/XXX_create_organization_customers.rb (with unique index)
|
|
15
|
+
|
|
16
|
+
And adds to existing models:
|
|
17
|
+
Customer: has_many :organization_customers, has_many :organizations
|
|
18
|
+
Organization: has_many :organization_customers, has_many :customers
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--user User model name (required)
|
|
22
|
+
--entity Entity model name (required)
|
|
23
|
+
--roles Comma-separated roles (default: member,owner)
|
|
24
|
+
--extra-attributes Additional model attributes
|
|
25
|
+
--dest Destination package (default: main_app)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "../lib/plutonium_generators"
|
|
5
|
+
|
|
6
|
+
module Pu
|
|
7
|
+
module Saas
|
|
8
|
+
class MembershipGenerator < ::Rails::Generators::Base
|
|
9
|
+
include PlutoniumGenerators::Generator
|
|
10
|
+
|
|
11
|
+
desc "Generate a SaaS membership model linking users to entities"
|
|
12
|
+
|
|
13
|
+
class_option :user, type: :string, required: true,
|
|
14
|
+
desc: "The user model name (e.g., Customer)"
|
|
15
|
+
|
|
16
|
+
class_option :entity, type: :string, required: true,
|
|
17
|
+
desc: "The entity model name (e.g., Organization)"
|
|
18
|
+
|
|
19
|
+
class_option :roles, type: :array, default: %w[member owner],
|
|
20
|
+
desc: "Available roles for memberships"
|
|
21
|
+
|
|
22
|
+
class_option :extra_attributes, type: :array, default: [],
|
|
23
|
+
desc: "Additional attributes for the membership model"
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
validate_models_exist!
|
|
27
|
+
generate_membership_model
|
|
28
|
+
add_unique_index_to_migration
|
|
29
|
+
add_default_to_role_column
|
|
30
|
+
add_role_enum_to_model
|
|
31
|
+
add_unique_validation_to_model
|
|
32
|
+
add_associations_to_models
|
|
33
|
+
rescue => e
|
|
34
|
+
exception "#{self.class} failed:", e
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate_models_exist!
|
|
40
|
+
user_model_path = File.join("app", "models", "#{normalized_user_name}.rb")
|
|
41
|
+
entity_model_path = File.join("app", "models", "#{normalized_entity_name}.rb")
|
|
42
|
+
|
|
43
|
+
unless File.exist?(user_model_path)
|
|
44
|
+
raise "User model '#{normalized_user_name}' does not exist at #{user_model_path}. Please create it first with: rails g pu:saas:user #{options[:user]}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
unless File.exist?(entity_model_path)
|
|
48
|
+
raise "Entity model '#{normalized_entity_name}' does not exist at #{entity_model_path}. Please create it first with: rails g pu:saas:entity #{options[:entity]}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def generate_membership_model
|
|
53
|
+
invoke "pu:res:model", [membership_model_name, *membership_attributes],
|
|
54
|
+
dest: selected_destination_feature,
|
|
55
|
+
force: options[:force],
|
|
56
|
+
skip: options[:skip]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_unique_index_to_migration
|
|
60
|
+
migration_file = find_migration_file
|
|
61
|
+
|
|
62
|
+
return unless migration_file
|
|
63
|
+
|
|
64
|
+
insert_into_file migration_file,
|
|
65
|
+
indent("add_index :#{membership_table_name}, [:#{normalized_entity_name}_id, :#{normalized_user_name}_id], unique: true\n", 4),
|
|
66
|
+
before: /^ end\s*$/
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def add_default_to_role_column
|
|
70
|
+
migration_file = find_migration_file
|
|
71
|
+
|
|
72
|
+
return unless migration_file
|
|
73
|
+
|
|
74
|
+
gsub_file migration_file,
|
|
75
|
+
/t\.integer :role, null: false/,
|
|
76
|
+
"t.integer :role, null: false, default: 0"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_role_enum_to_model
|
|
80
|
+
model_file = File.join("app", "models", "#{membership_model_name}.rb")
|
|
81
|
+
|
|
82
|
+
return unless File.exist?(model_file)
|
|
83
|
+
|
|
84
|
+
enum_definition = "enum :role, #{roles_enum}\n"
|
|
85
|
+
insert_into_file model_file, indent(enum_definition, 2), before: /^\s*# add enums above\./
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_unique_validation_to_model
|
|
89
|
+
model_file = File.join("app", "models", "#{membership_model_name}.rb")
|
|
90
|
+
|
|
91
|
+
return unless File.exist?(model_file)
|
|
92
|
+
|
|
93
|
+
validation = "validates :#{normalized_user_name}, uniqueness: {scope: :#{normalized_entity_name}_id, message: \"is already a member of this #{normalized_entity_name.humanize.downcase}\"}\n"
|
|
94
|
+
insert_into_file model_file, indent(validation, 2), before: /^\s*# add validations above\./
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_associations_to_models
|
|
98
|
+
add_association_to_entity_model
|
|
99
|
+
add_association_to_user_model
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_association_to_entity_model
|
|
103
|
+
entity_model_path = File.join("app", "models", "#{normalized_entity_name}.rb")
|
|
104
|
+
|
|
105
|
+
return unless File.exist?(entity_model_path)
|
|
106
|
+
|
|
107
|
+
associations = <<~RUBY
|
|
108
|
+
has_many :#{membership_table_name}, dependent: :destroy
|
|
109
|
+
has_many :#{normalized_user_name.pluralize}, through: :#{membership_table_name}
|
|
110
|
+
RUBY
|
|
111
|
+
insert_into_file entity_model_path, indent(associations, 2), before: /# add has_many associations above\.\n/
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def add_association_to_user_model
|
|
115
|
+
user_model_path = File.join("app", "models", "#{normalized_user_name}.rb")
|
|
116
|
+
|
|
117
|
+
return unless File.exist?(user_model_path)
|
|
118
|
+
|
|
119
|
+
associations = <<~RUBY
|
|
120
|
+
has_many :#{membership_table_name}, dependent: :destroy
|
|
121
|
+
has_many :#{normalized_entity_name.pluralize}, through: :#{membership_table_name}
|
|
122
|
+
RUBY
|
|
123
|
+
insert_into_file user_model_path, indent(associations, 2), before: /# add has_many associations above\.\n/
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def find_migration_file
|
|
127
|
+
migration_dir = File.join("db", "migrate")
|
|
128
|
+
Dir[File.join(migration_dir, "*_create_#{membership_table_name}.rb")].first
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def membership_model_name
|
|
132
|
+
"#{normalized_entity_name}_#{normalized_user_name}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def membership_table_name
|
|
136
|
+
membership_model_name.pluralize
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def membership_attributes
|
|
140
|
+
[
|
|
141
|
+
"#{normalized_entity_name}:references",
|
|
142
|
+
"#{normalized_user_name}:references",
|
|
143
|
+
"role:integer",
|
|
144
|
+
*Array(options[:extra_attributes])
|
|
145
|
+
]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def normalized_user_name = options[:user].underscore
|
|
149
|
+
|
|
150
|
+
def normalized_entity_name = options[:entity].underscore
|
|
151
|
+
|
|
152
|
+
def roles
|
|
153
|
+
Array(options[:roles]).flat_map { |r| r.split(",") }.map(&:strip)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def roles_enum
|
|
157
|
+
roles.each_with_index.map { |r, i| "#{r}: #{i}" }.join(", ")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def selected_destination_feature
|
|
161
|
+
feature_option :dest, prompt: "Select destination feature"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate a complete SaaS setup with user account, entity, and membership.
|
|
3
|
+
This is the recommended way to set up multi-tenant authentication.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
rails g pu:saas:setup --user Customer --entity Organization
|
|
7
|
+
rails g pu:saas:setup --user Customer --entity Organization --roles=member,admin,owner
|
|
8
|
+
rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
|
|
9
|
+
rails g pu:saas:setup --user Customer --entity Organization --user-attributes=name:string
|
|
10
|
+
rails g pu:saas:setup --user Customer --entity Organization --skip-entity
|
|
11
|
+
|
|
12
|
+
This creates:
|
|
13
|
+
1. User account (Customer) with Rodauth authentication
|
|
14
|
+
2. Entity model (Organization) with unique name
|
|
15
|
+
3. Membership model (OrganizationCustomer) with role enum
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--user User model name (required)
|
|
19
|
+
--entity Entity model name (required)
|
|
20
|
+
--allow-signup Allow public registration (default: true)
|
|
21
|
+
--roles Comma-separated membership roles (default: member,owner)
|
|
22
|
+
--skip-entity Skip entity model generation
|
|
23
|
+
--skip-membership Skip membership model generation
|
|
24
|
+
--user-attributes Additional user model attributes
|
|
25
|
+
--entity-attributes Additional entity model attributes
|
|
26
|
+
--membership-attributes Additional membership model attributes
|
|
27
|
+
--dest Destination package (default: main_app)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return unless defined?(Rodauth::Rails)
|
|
4
|
+
|
|
5
|
+
require "rails/generators/base"
|
|
6
|
+
require_relative "../lib/plutonium_generators"
|
|
7
|
+
|
|
8
|
+
module Pu
|
|
9
|
+
module Saas
|
|
10
|
+
class SetupGenerator < ::Rails::Generators::Base
|
|
11
|
+
include PlutoniumGenerators::Generator
|
|
12
|
+
|
|
13
|
+
desc "Generate a complete SaaS setup with user, entity, and membership"
|
|
14
|
+
|
|
15
|
+
class_option :user, type: :string, required: true,
|
|
16
|
+
desc: "The user model name (e.g., Customer)"
|
|
17
|
+
|
|
18
|
+
class_option :entity, type: :string, required: true,
|
|
19
|
+
desc: "The entity model name (e.g., Organization)"
|
|
20
|
+
|
|
21
|
+
class_option :allow_signup, type: :boolean, default: true,
|
|
22
|
+
desc: "Whether to allow users to sign up to the platform"
|
|
23
|
+
|
|
24
|
+
class_option :roles, type: :array, default: %w[member owner],
|
|
25
|
+
desc: "Available roles for memberships"
|
|
26
|
+
|
|
27
|
+
class_option :skip_entity, type: :boolean, default: false,
|
|
28
|
+
desc: "Skip entity model generation"
|
|
29
|
+
|
|
30
|
+
class_option :skip_membership, type: :boolean, default: false,
|
|
31
|
+
desc: "Skip membership model generation"
|
|
32
|
+
|
|
33
|
+
class_option :user_attributes, type: :array, default: [],
|
|
34
|
+
desc: "Additional attributes for the user model (e.g., name:string)"
|
|
35
|
+
|
|
36
|
+
class_option :entity_attributes, type: :array, default: [],
|
|
37
|
+
desc: "Additional attributes for the entity model"
|
|
38
|
+
|
|
39
|
+
class_option :membership_attributes, type: :array, default: [],
|
|
40
|
+
desc: "Additional attributes for the membership model"
|
|
41
|
+
|
|
42
|
+
def start
|
|
43
|
+
generate_user
|
|
44
|
+
generate_entity unless options[:skip_entity]
|
|
45
|
+
generate_membership unless options[:skip_membership]
|
|
46
|
+
rescue => e
|
|
47
|
+
exception "#{self.class} failed:", e
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def generate_user
|
|
53
|
+
# Use class-based invocation to avoid Thor's invoke caching
|
|
54
|
+
klass = Rails::Generators.find_by_namespace("pu:saas:user")
|
|
55
|
+
klass.new(
|
|
56
|
+
[options[:user]],
|
|
57
|
+
{
|
|
58
|
+
allow_signup: options[:allow_signup],
|
|
59
|
+
extra_attributes: options[:user_attributes],
|
|
60
|
+
force: options[:force],
|
|
61
|
+
skip: options[:skip]
|
|
62
|
+
}
|
|
63
|
+
).invoke_all
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def generate_entity
|
|
67
|
+
# Use class-based invocation to avoid Thor's invoke caching
|
|
68
|
+
klass = Rails::Generators.find_by_namespace("pu:saas:entity")
|
|
69
|
+
klass.new(
|
|
70
|
+
[options[:entity]],
|
|
71
|
+
{
|
|
72
|
+
extra_attributes: options[:entity_attributes],
|
|
73
|
+
dest: options[:dest],
|
|
74
|
+
force: options[:force],
|
|
75
|
+
skip: options[:skip]
|
|
76
|
+
}
|
|
77
|
+
).invoke_all
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def generate_membership
|
|
81
|
+
# Use class-based invocation to avoid Thor's invoke caching
|
|
82
|
+
klass = Rails::Generators.find_by_namespace("pu:saas:membership")
|
|
83
|
+
klass.new(
|
|
84
|
+
[],
|
|
85
|
+
{
|
|
86
|
+
user: options[:user],
|
|
87
|
+
entity: options[:entity],
|
|
88
|
+
roles: options[:roles],
|
|
89
|
+
extra_attributes: options[:membership_attributes],
|
|
90
|
+
dest: options[:dest],
|
|
91
|
+
force: options[:force],
|
|
92
|
+
skip: options[:skip]
|
|
93
|
+
}
|
|
94
|
+
).invoke_all
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Generate a SaaS user account with Rodauth authentication.
|
|
3
|
+
Creates a user model with login, signup, password reset, and other auth features.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
rails g pu:saas:user Customer
|
|
7
|
+
rails g pu:saas:user Customer --no-allow-signup
|
|
8
|
+
rails g pu:saas:user Customer --extra-attributes=name:string,phone:string
|
|
9
|
+
|
|
10
|
+
This creates:
|
|
11
|
+
app/models/customer.rb
|
|
12
|
+
app/rodauth/customer_rodauth_plugin.rb
|
|
13
|
+
app/controllers/rodauth/customer_controller.rb
|
|
14
|
+
app/controllers/customers_controller.rb
|
|
15
|
+
app/policies/customer_policy.rb
|
|
16
|
+
app/definitions/customer_definition.rb
|
|
17
|
+
db/migrate/XXX_create_rodauth_customer_*.rb
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--allow-signup Allow public registration (default: true)
|
|
21
|
+
--extra-attributes Additional model attributes (e.g., name:string)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return unless defined?(Rodauth::Rails)
|
|
4
|
+
|
|
5
|
+
require "rails/generators/named_base"
|
|
6
|
+
require_relative "../lib/plutonium_generators"
|
|
7
|
+
|
|
8
|
+
module Pu
|
|
9
|
+
module Saas
|
|
10
|
+
class UserGenerator < ::Rails::Generators::NamedBase
|
|
11
|
+
include PlutoniumGenerators::Concerns::Logger
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
desc "Generate a SaaS user account with Rodauth integration"
|
|
16
|
+
|
|
17
|
+
class_option :allow_signup, type: :boolean, default: true,
|
|
18
|
+
desc: "Whether to allow users to sign up to the platform"
|
|
19
|
+
|
|
20
|
+
class_option :extra_attributes, type: :array, default: [],
|
|
21
|
+
desc: "Additional attributes to add to the account model (e.g., name:string)"
|
|
22
|
+
|
|
23
|
+
def start
|
|
24
|
+
generate_user_account
|
|
25
|
+
rescue => e
|
|
26
|
+
exception "#{self.class} failed:", e
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def generate_user_account
|
|
32
|
+
invoke "pu:rodauth:account", [name],
|
|
33
|
+
defaults: false,
|
|
34
|
+
**user_features,
|
|
35
|
+
extra_attributes: Array(options[:extra_attributes]),
|
|
36
|
+
force: options[:force],
|
|
37
|
+
skip: options[:skip],
|
|
38
|
+
lint: true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def user_features
|
|
42
|
+
features = %i[
|
|
43
|
+
login
|
|
44
|
+
remember
|
|
45
|
+
logout
|
|
46
|
+
create_account
|
|
47
|
+
verify_account
|
|
48
|
+
verify_account_grace_period
|
|
49
|
+
reset_password
|
|
50
|
+
reset_password_notify
|
|
51
|
+
change_login
|
|
52
|
+
verify_login_change
|
|
53
|
+
change_password
|
|
54
|
+
change_password_notify
|
|
55
|
+
case_insensitive_login
|
|
56
|
+
internal_request
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
features.delete(:create_account) unless options[:allow_signup]
|
|
60
|
+
features.map { |feature| [feature, true] }.to_h
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalized_name = name.underscore
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -154,19 +154,22 @@ module Plutonium
|
|
|
154
154
|
# Determine the helper name based on action and route type
|
|
155
155
|
# For singular routes (has_one), always use the association name as-is (no singularize)
|
|
156
156
|
# For plural routes (has_many):
|
|
157
|
-
# - :index
|
|
157
|
+
# - :index/:create actions use plural (blogging_post_nested_comments) - collection routes
|
|
158
158
|
# - :new action uses singular (new_blogging_post_nested_comment)
|
|
159
|
-
# - member actions (show/edit/destroy) use singular (blogging_post_nested_comment)
|
|
159
|
+
# - member actions (show/edit/update/destroy) use singular (blogging_post_nested_comment)
|
|
160
|
+
is_collection_action = action == :index || action == :create || (no_record && action != :new)
|
|
160
161
|
helper_base = if is_singular
|
|
161
162
|
"#{parent_singular}_#{nested_resource_name}"
|
|
162
|
-
elsif
|
|
163
|
+
elsif is_collection_action
|
|
163
164
|
"#{parent_singular}_#{nested_resource_name}"
|
|
164
165
|
else
|
|
165
166
|
"#{parent_singular}_#{nested_resource_name.to_s.singularize}"
|
|
166
167
|
end
|
|
167
168
|
|
|
169
|
+
# Only add helper prefix for actions that have named route helpers (new, edit)
|
|
170
|
+
# :create/:update use HTTP method to differentiate, not route helper prefix
|
|
168
171
|
helper_suffix = case action
|
|
169
|
-
when :show, nil then ""
|
|
172
|
+
when :show, :create, :update, nil then ""
|
|
170
173
|
else "#{action}_"
|
|
171
174
|
end
|
|
172
175
|
|
|
@@ -175,7 +178,8 @@ module Plutonium
|
|
|
175
178
|
# Build the arguments for the helper
|
|
176
179
|
helper_args = [parent.to_param]
|
|
177
180
|
# Include element ID for plural routes (has_many) when we have a record instance
|
|
178
|
-
|
|
181
|
+
# Skip ID for collection actions (:index, :create) which don't need a member ID
|
|
182
|
+
unless is_singular || no_record || is_collection_action
|
|
179
183
|
helper_args << element.to_param
|
|
180
184
|
end
|
|
181
185
|
|
|
@@ -13,7 +13,8 @@ module Plutonium
|
|
|
13
13
|
# field :name, as: :string
|
|
14
14
|
# input :email, as: :email
|
|
15
15
|
# filter :status, type: :select, collection: %w[active inactive]
|
|
16
|
-
# scope :active
|
|
16
|
+
# scope :active
|
|
17
|
+
# default_scope :active
|
|
17
18
|
# sorter :created_at
|
|
18
19
|
#
|
|
19
20
|
# def customize_fields
|
|
@@ -29,6 +30,7 @@ module Plutonium
|
|
|
29
30
|
include InheritableConfigAttr
|
|
30
31
|
include Actions
|
|
31
32
|
include Sorting
|
|
33
|
+
include Scoping
|
|
32
34
|
include Search
|
|
33
35
|
include NestedInputs
|
|
34
36
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Definition
|
|
3
|
+
module Scoping
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
class_attribute :_default_scope, instance_writer: false, instance_predicate: false
|
|
8
|
+
|
|
9
|
+
def self.default_scope(name = nil)
|
|
10
|
+
self._default_scope = name.to_sym if name
|
|
11
|
+
_default_scope
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def default_scope
|
|
16
|
+
self.class._default_scope
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Invites
|
|
5
|
+
module Concerns
|
|
6
|
+
# CancelInvite provides the cancel invitation interaction logic.
|
|
7
|
+
#
|
|
8
|
+
# Include this concern in your cancel interaction and override methods
|
|
9
|
+
# as needed for customization.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# class CancelInviteInteraction < Plutonium::Resource::Interaction
|
|
13
|
+
# include Plutonium::Invites::Concerns::CancelInvite
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
module CancelInvite
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
presents label: "Cancel Invitation", icon: "outline/x-circle"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def execute
|
|
24
|
+
unless resource.pending?
|
|
25
|
+
return failed(not_pending_message)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
resource.cancelled!
|
|
29
|
+
succeed(resource).with_message(success_message)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def success_message
|
|
35
|
+
"Invitation cancelled"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def not_pending_message
|
|
39
|
+
"Can only cancel pending invitations"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|