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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-connect-resource/SKILL.md +19 -1
  3. data/.claude/skills/plutonium-controller/SKILL.md +5 -9
  4. data/.claude/skills/plutonium-definition-query/SKILL.md +10 -2
  5. data/.claude/skills/plutonium-installation/SKILL.md +9 -7
  6. data/.claude/skills/plutonium-invites/SKILL.md +363 -0
  7. data/.claude/skills/plutonium-package/SKILL.md +2 -1
  8. data/.claude/skills/plutonium-portal/SKILL.md +30 -16
  9. data/.claude/skills/plutonium-rodauth/SKILL.md +111 -18
  10. data/CHANGELOG.md +48 -0
  11. data/app/assets/plutonium.css +1 -1
  12. data/config/initializers/sqlite_alias.rb +8 -8
  13. data/docs/.vitepress/config.ts +1 -0
  14. data/docs/getting-started/tutorial/07-author-portal.md +1 -0
  15. data/docs/getting-started/tutorial/08-customizing-ui.md +5 -2
  16. data/docs/guides/adding-resources.md +10 -0
  17. data/docs/guides/authentication.md +15 -8
  18. data/docs/guides/creating-packages.md +13 -8
  19. data/docs/guides/index.md +2 -0
  20. data/docs/guides/search-filtering.md +8 -3
  21. data/docs/guides/user-invites.md +497 -0
  22. data/docs/public/templates/base.rb +5 -1
  23. data/docs/public/templates/lite.rb +42 -0
  24. data/docs/public/templates/pluton8.rb +7 -2
  25. data/docs/reference/controller/index.md +12 -7
  26. data/docs/reference/definition/query.md +12 -3
  27. data/docs/reference/generators/index.md +70 -10
  28. data/docs/reference/portal/index.md +22 -11
  29. data/gemfiles/rails_7.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  31. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  32. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +31 -0
  33. data/lib/generators/pu/gem/active_shrine/templates/config/initializers/shrine.rb.tt +58 -0
  34. data/lib/generators/pu/gem/annotated/templates/lib/tasks/auto_annotate_models.rake +6 -1
  35. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -0
  36. data/lib/generators/pu/invites/USAGE +27 -0
  37. data/lib/generators/pu/invites/install_generator.rb +364 -0
  38. data/lib/generators/pu/invites/invitable/USAGE +31 -0
  39. data/lib/generators/pu/invites/invitable_generator.rb +143 -0
  40. data/lib/generators/pu/invites/templates/INSTRUCTIONS +22 -0
  41. data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +24 -0
  42. data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +26 -0
  43. data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +47 -0
  44. data/lib/generators/pu/invites/templates/invitable/invitation.html.erb.tt +45 -0
  45. data/lib/generators/pu/invites/templates/invitable/invitation.text.erb.tt +15 -0
  46. data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +33 -0
  47. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +77 -0
  48. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +68 -0
  49. data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +23 -0
  50. data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +7 -0
  51. data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/resend_invite_interaction.rb.tt +7 -0
  52. data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +34 -0
  53. data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +41 -0
  54. data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +33 -0
  55. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/error.html.erb.tt +24 -0
  56. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +40 -0
  57. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +39 -0
  58. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +49 -0
  59. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +45 -0
  60. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +15 -0
  61. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +23 -0
  62. data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +33 -0
  63. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +23 -2
  64. data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +130 -0
  65. data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +72 -0
  66. data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -2
  67. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +7 -1
  68. data/lib/generators/pu/lite/litestream/litestream_generator.rb +105 -0
  69. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +88 -0
  70. data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +14 -0
  71. data/lib/generators/pu/lite/setup/setup_generator.rb +54 -0
  72. data/lib/generators/pu/lite/solid_cable/solid_cable_generator.rb +65 -0
  73. data/lib/generators/pu/lite/solid_cache/solid_cache_generator.rb +66 -0
  74. data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +61 -0
  75. data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +107 -0
  76. data/lib/generators/pu/pkg/portal/USAGE +8 -2
  77. data/lib/generators/pu/pkg/portal/portal_generator.rb +11 -1
  78. data/lib/generators/pu/pkg/portal/templates/app/controllers/concerns/controller.rb.tt +2 -0
  79. data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +1 -0
  80. data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +7 -0
  81. data/lib/generators/pu/pkg/portal/templates/lib/engine.rb.tt +3 -0
  82. data/lib/generators/pu/res/conn/USAGE +5 -0
  83. data/lib/generators/pu/res/conn/conn_generator.rb +30 -4
  84. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +6 -3
  85. data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +6 -6
  86. data/lib/generators/pu/rodauth/account_generator.rb +36 -11
  87. data/lib/generators/pu/rodauth/admin_generator.rb +55 -0
  88. data/lib/generators/pu/rodauth/install_generator.rb +1 -8
  89. data/lib/generators/pu/rodauth/templates/app/interactions/invite_admin_interaction.rb.tt +25 -0
  90. data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +6 -2
  91. data/lib/generators/pu/saas/USAGE +22 -0
  92. data/lib/generators/pu/saas/entity/USAGE +19 -0
  93. data/lib/generators/pu/saas/entity_generator.rb +55 -0
  94. data/lib/generators/pu/saas/membership/USAGE +25 -0
  95. data/lib/generators/pu/saas/membership_generator.rb +165 -0
  96. data/lib/generators/pu/saas/setup/USAGE +27 -0
  97. data/lib/generators/pu/saas/setup_generator.rb +98 -0
  98. data/lib/generators/pu/saas/user/USAGE +21 -0
  99. data/lib/generators/pu/saas/user_generator.rb +66 -0
  100. data/lib/plutonium/core/controller.rb +9 -5
  101. data/lib/plutonium/definition/base.rb +3 -1
  102. data/lib/plutonium/definition/scoping.rb +20 -0
  103. data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
  104. data/lib/plutonium/invites/concerns/invitable.rb +98 -0
  105. data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
  106. data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
  107. data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
  108. data/lib/plutonium/invites/controller.rb +226 -0
  109. data/lib/plutonium/invites/pending_invite_check.rb +76 -0
  110. data/lib/plutonium/invites.rb +6 -0
  111. data/lib/plutonium/resource/controllers/queryable.rb +4 -0
  112. data/lib/plutonium/resource/query_object.rb +3 -5
  113. data/lib/plutonium/version.rb +1 -1
  114. data/package.json +1 -1
  115. metadata +64 -7
  116. data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
  117. data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
  118. data/public/plutonium-assets/plutonium-logo-original.png +0 -0
  119. data/public/plutonium-assets/plutonium-logo-white.png +0 -0
  120. 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 action uses plural (blogging_post_nested_comments)
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 action == :index || (no_record && action != :new)
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
- unless is_singular || no_record
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, default: true
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