plutonium 0.33.1 → 0.34.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -0,0 +1,118 @@
1
+ # Chapter 1: Project Setup
2
+
3
+ In this chapter, you'll create a new Plutonium application and explore its structure.
4
+
5
+ ## Creating the Application
6
+
7
+ Open your terminal and run:
8
+
9
+ ```bash
10
+ rails new blog -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
11
+ ```
12
+
13
+ This creates a new Rails application with Plutonium pre-configured.
14
+
15
+ When prompted, accept the defaults or customize as needed. The template will:
16
+ - Install the Plutonium gem
17
+ - Configure TailwindCSS 4
18
+ - Set up Rodauth for authentication
19
+ - Create the database schema
20
+
21
+ ## Starting the Server
22
+
23
+ ```bash
24
+ cd blog
25
+ bin/dev
26
+ ```
27
+
28
+ Visit `http://localhost:3000`. You should see a welcome page.
29
+
30
+ ## Project Structure
31
+
32
+ Let's explore what was created:
33
+
34
+ ```
35
+ blog/
36
+ ├── app/
37
+ │ ├── controllers/ # Standard Rails controllers
38
+ │ ├── definitions/ # Plutonium definitions (how resources render)
39
+ │ ├── interactions/ # Business logic classes
40
+ │ ├── models/ # Standard Rails models
41
+ │ ├── policies/ # Authorization policies
42
+ │ └── views/ # Phlex view components
43
+ ├── packages/ # Feature packages and portals
44
+ │ └── .keep
45
+ ├── config/
46
+ │ └── initializers/
47
+ │ └── plutonium.rb # Plutonium configuration
48
+ └── ...
49
+ ```
50
+
51
+ ### Key Directories
52
+
53
+ **`app/definitions/`** - Contains Definition classes that control how resources are displayed. Fields, forms, tables, and actions are configured here.
54
+
55
+ **`app/policies/`** - Contains Policy classes that control authorization. Who can do what with each resource.
56
+
57
+ **`app/interactions/`** - Contains Interaction classes for complex business logic. Used for custom actions beyond simple CRUD.
58
+
59
+ **`packages/`** - Contains Feature Packages (business logic modules) and Portal Packages (web interfaces).
60
+
61
+ ## Understanding Packages
62
+
63
+ Plutonium uses two types of packages:
64
+
65
+ ### Feature Packages
66
+
67
+ Feature packages contain your business logic - models, definitions, policies, interactions, and controllers. They're Rails engines that can be shared across multiple portals.
68
+
69
+ ```
70
+ packages/
71
+ └── blogging/ # Feature package
72
+ ├── app/
73
+ │ ├── controllers/blogging/
74
+ │ ├── definitions/blogging/
75
+ │ ├── interactions/blogging/
76
+ │ ├── models/blogging/
77
+ │ ├── policies/blogging/
78
+ │ └── views/blogging/
79
+ └── lib/
80
+ └── engine.rb
81
+ ```
82
+
83
+ ### Portal Packages
84
+
85
+ Portal packages are web interfaces that expose resources to users. Each portal can have its own authentication, authorization rules, and UI customizations.
86
+
87
+ ```
88
+ packages/
89
+ └── admin_portal/ # Portal package
90
+ ├── app/
91
+ │ ├── controllers/admin_portal/
92
+ │ └── views/admin_portal/
93
+ └── lib/admin_portal/
94
+ └── engine.rb
95
+ ```
96
+
97
+ ## Configuration
98
+
99
+ Open `config/initializers/plutonium.rb`:
100
+
101
+ ```ruby
102
+ Plutonium.configure do |config|
103
+ config.load_defaults 1.0
104
+
105
+ # Configure plutonium above.
106
+ end
107
+ ```
108
+
109
+ The defaults work well out of the box. Available options include:
110
+ - `config.development` - Enable development mode
111
+ - `config.cache_discovery` - Cache resource discovery (auto-configured per environment)
112
+ - `config.assets.logo` - Custom logo path
113
+
114
+ ## What's Next
115
+
116
+ In the next chapter, we'll create our first resource - the Post model with its definition and policy.
117
+
118
+ [Continue to Chapter 2: Creating Your First Resource →](./02-first-resource)
@@ -0,0 +1,180 @@
1
+ # Chapter 2: Creating Your First Resource
2
+
3
+ In this chapter, you'll create the Post resource - the core of our blog application.
4
+
5
+ ## What is a Resource?
6
+
7
+ In Plutonium, a **resource** is a complete unit consisting of:
8
+ - **Model** - The data structure (ActiveRecord)
9
+ - **Definition** - How the resource renders (fields, actions, UI)
10
+ - **Policy** - Who can do what (authorization)
11
+ - **Controller** - HTTP handling (auto-generated or custom)
12
+
13
+ ## Generating a Feature Package
14
+
15
+ First, let's create a feature package to hold our blogging logic:
16
+
17
+ ```bash
18
+ rails generate pu:pkg:package blogging
19
+ ```
20
+
21
+ This creates:
22
+
23
+ ```
24
+ packages/blogging/
25
+ ├── app/
26
+ │ ├── controllers/blogging/
27
+ │ ├── definitions/blogging/
28
+ │ ├── interactions/blogging/
29
+ │ ├── models/blogging/
30
+ │ ├── policies/blogging/
31
+ │ └── views/blogging/
32
+ └── lib/
33
+ └── engine.rb
34
+ ```
35
+
36
+ ## Generating the Post Resource
37
+
38
+ Now generate the Post resource inside the blogging package:
39
+
40
+ ```bash
41
+ rails generate pu:res:scaffold Post title:string body:text 'published:boolean?' --dest=blogging
42
+ ```
43
+
44
+ This creates:
45
+
46
+ ### Model (`packages/blogging/app/models/blogging/post.rb`)
47
+
48
+ ```ruby
49
+ class Blogging::Post < Blogging::ResourceRecord
50
+ # Add validations, associations, and business logic here
51
+ end
52
+ ```
53
+
54
+ ### Definition (`packages/blogging/app/definitions/blogging/post_definition.rb`)
55
+
56
+ ```ruby
57
+ class Blogging::PostDefinition < Blogging::ResourceDefinition
58
+ # Customize field rendering, forms, and UI here
59
+ end
60
+ ```
61
+
62
+ ### Policy (`packages/blogging/app/policies/blogging/post_policy.rb`)
63
+
64
+ ```ruby
65
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
66
+ def create?
67
+ true
68
+ end
69
+
70
+ def read?
71
+ true
72
+ end
73
+
74
+ def permitted_attributes_for_create
75
+ [:title, :body, :published]
76
+ end
77
+
78
+ def permitted_attributes_for_read
79
+ [:title, :body, :published]
80
+ end
81
+
82
+ def permitted_associations
83
+ %i[]
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Migration
89
+
90
+ ```ruby
91
+ class CreateBloggingPosts < ActiveRecord::Migration[8.0]
92
+ def change
93
+ create_table :blogging_posts do |t|
94
+ t.string :title, null: false
95
+ t.text :body, null: false
96
+ t.boolean :published
97
+
98
+ t.timestamps
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ ## Running the Migration
105
+
106
+ ```bash
107
+ rails db:migrate
108
+ ```
109
+
110
+ ## Creating a Portal
111
+
112
+ Resources need a portal to be accessible via the web. Let's create an admin portal:
113
+
114
+ ```bash
115
+ rails generate pu:pkg:portal admin
116
+ ```
117
+
118
+ This creates the AdminPortal package with authentication configured.
119
+
120
+ ## Connecting the Resource
121
+
122
+ Connect the Post resource to the admin portal:
123
+
124
+ ```bash
125
+ rails generate pu:res:conn Blogging::Post --dest=admin_portal
126
+ ```
127
+
128
+ This:
129
+ 1. Adds routes for Post in the admin portal
130
+ 2. Creates a portal-specific controller
131
+ 3. Registers the resource with the portal
132
+
133
+ ## Starting the Server
134
+
135
+ ```bash
136
+ bin/dev
137
+ ```
138
+
139
+ Visit `http://localhost:3000/admin/blogging/posts`. You should see:
140
+ - An empty posts table
141
+ - A "New Post" button
142
+ - Search and filter options
143
+
144
+ Try creating a post. The form is automatically generated from your model's attributes.
145
+
146
+ ## Understanding Auto-Detection
147
+
148
+ Notice that we wrote almost no configuration code. Plutonium auto-detected:
149
+
150
+ - **Fields** from the model's columns
151
+ - **Validations** from the model's validators
152
+ - **Associations** from belongs_to/has_many
153
+ - **Form inputs** based on column types
154
+ - **Table columns** from the policy's permitted attributes
155
+
156
+ This is Plutonium's core philosophy: **convention over configuration**. You only write code when you need to change the defaults.
157
+
158
+ ## Customizing Table Columns
159
+
160
+ By default, Plutonium shows all permitted attributes in the table. To customize which columns appear, use the policy's `permitted_attributes_for_index` method:
161
+
162
+ ```ruby
163
+ # packages/blogging/app/policies/blogging/post_policy.rb
164
+ module Blogging
165
+ class PostPolicy < Blogging::ResourcePolicy
166
+ # Control which columns appear in the index table
167
+ def permitted_attributes_for_index
168
+ [:title, :published, :created_at]
169
+ end
170
+ end
171
+ end
172
+ ```
173
+
174
+ Refresh the page. The table now shows only the columns you specified.
175
+
176
+ ## What's Next
177
+
178
+ Our resource is working, but anyone can access it. In the next chapter, we'll add authentication with Rodauth.
179
+
180
+ [Continue to Chapter 3: Setting Up Authentication →](./03-authentication)
@@ -0,0 +1,246 @@
1
+ # Chapter 3: Setting Up Authentication
2
+
3
+ In this chapter, you'll configure Rodauth for user authentication.
4
+
5
+ ## What is Rodauth?
6
+
7
+ Rodauth is a Ruby authentication framework that Plutonium uses for:
8
+ - User registration and login
9
+ - Password reset and email verification
10
+ - Multi-factor authentication
11
+ - Session management
12
+
13
+ Plutonium integrates Rodauth seamlessly with its portal system.
14
+
15
+ ## Setting Up Authentication
16
+
17
+ If you used the Plutonium template, Rodauth is already installed. If not:
18
+
19
+ ```bash
20
+ rails generate pu:rodauth:install
21
+ rails db:migrate
22
+ ```
23
+
24
+ ## Creating an Account Type
25
+
26
+ Plutonium supports multiple account types. For admin accounts that cannot self-register, use the `admin` generator:
27
+
28
+ ```bash
29
+ rails generate pu:rodauth:admin admin
30
+ ```
31
+
32
+ This creates:
33
+
34
+ ### Account Model (`app/models/admin.rb`)
35
+
36
+ ```ruby
37
+ class Admin < ResourceRecord
38
+ include Rodauth::Rails.model(:admin)
39
+ end
40
+ ```
41
+
42
+ ### Rodauth Plugin (`app/rodauth/admin_rodauth_plugin.rb`)
43
+
44
+ ```ruby
45
+ class AdminRodauthPlugin < RodauthPlugin
46
+ configure do
47
+ # Multi-phase login, 2FA, lockout, audit logging enabled
48
+ enable :login, :remember, :logout, ...
49
+
50
+ # No public signup - accounts created via rake task
51
+ before_create_account_route do
52
+ request.halt unless internal_request?
53
+ end
54
+ end
55
+ end
56
+ ```
57
+
58
+ The generator also creates migrations for the account table and authentication features.
59
+
60
+ ## Configuring the Portal
61
+
62
+ Authentication is configured in the portal's controller concern. Update it to use Rodauth:
63
+
64
+ ```ruby
65
+ # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
66
+ module AdminPortal
67
+ module Concerns
68
+ module Controller
69
+ extend ActiveSupport::Concern
70
+ include Plutonium::Portal::Controller
71
+ include Plutonium::Auth::Rodauth(:admin)
72
+ end
73
+ end
74
+ end
75
+ ```
76
+
77
+ This provides `current_user` and authentication helpers throughout the portal.
78
+
79
+ ## Running Migrations
80
+
81
+ ```bash
82
+ rails db:migrate
83
+ ```
84
+
85
+ ## Testing Authentication
86
+
87
+ Restart your server:
88
+
89
+ ```bash
90
+ bin/dev
91
+ ```
92
+
93
+ Visit `http://localhost:3000/admin/blogging/posts`. You'll be redirected to the login page.
94
+
95
+ ### Creating an Admin Account
96
+
97
+ Admin accounts are created via rake task (web registration is disabled for security):
98
+
99
+ ```bash
100
+ rails rodauth:admin
101
+ # Or with EMAIL environment variable:
102
+ EMAIL=admin@example.com rails rodauth:admin
103
+ ```
104
+
105
+ The task will prompt for an email if not provided and send a verification email with password setup instructions.
106
+
107
+ Now log in with the account you created.
108
+
109
+ ## Customizing the Login Page
110
+
111
+ The login page uses Plutonium's default theme. To customize it:
112
+
113
+ ```ruby
114
+ # packages/admin_portal/app/views/admin_portal/auth/login.rb
115
+ module AdminPortal
116
+ module Auth
117
+ class Login < Plutonium::Auth::View::Login
118
+ def page_title
119
+ "Admin Login"
120
+ end
121
+
122
+ def welcome_message
123
+ "Sign in to manage your blog"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Email Configuration
131
+
132
+ For password reset and email verification, configure Action Mailer:
133
+
134
+ ```ruby
135
+ # config/environments/development.rb
136
+ config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
137
+ config.action_mailer.delivery_method = :letter_opener # Or :smtp for real emails
138
+ ```
139
+
140
+ ## Creating the User Account Type
141
+
142
+ Our blog needs users (authors) who can create posts. Let's create a User account type that allows self-registration:
143
+
144
+ ```bash
145
+ rails generate pu:rodauth:account user
146
+ rails db:migrate
147
+ ```
148
+
149
+ This creates a `User` model similar to `Admin`, but with public registration enabled.
150
+
151
+ ### Linking Posts to Users
152
+
153
+ Now we can add the author relationship to our Post model. Generate a migration:
154
+
155
+ ```bash
156
+ rails generate migration AddUserToBloggingPosts user:belongs_to
157
+ ```
158
+
159
+ Update the migration to add the foreign key:
160
+
161
+ ```ruby
162
+ class AddUserToBloggingPosts < ActiveRecord::Migration[8.0]
163
+ def change
164
+ add_reference :blogging_posts, :user, null: false, foreign_key: true
165
+ end
166
+ end
167
+ ```
168
+
169
+ Run the migration:
170
+
171
+ ```bash
172
+ rails db:migrate
173
+ ```
174
+
175
+ Update the Post model to include the association:
176
+
177
+ ```ruby
178
+ # packages/blogging/app/models/blogging/post.rb
179
+ class Blogging::Post < Blogging::ResourceRecord
180
+ belongs_to :user
181
+ # ... existing code
182
+ end
183
+ ```
184
+
185
+ Also update the Post policy to include user in permitted attributes:
186
+
187
+ ```ruby
188
+ # packages/blogging/app/policies/blogging/post_policy.rb
189
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
190
+ def permitted_attributes_for_create
191
+ [:title, :body, :published, :user_id]
192
+ end
193
+
194
+ def permitted_attributes_for_read
195
+ [:title, :body, :published, :user, :created_at]
196
+ end
197
+
198
+ def permitted_associations
199
+ %i[user]
200
+ end
201
+ end
202
+ ```
203
+
204
+ Now posts are linked to their authors, which we'll use for authorization in the next chapter.
205
+
206
+ ## Multiple Account Types
207
+
208
+ You can have different account types for different portals:
209
+
210
+ ```bash
211
+ # Create an author account type (if using a different name than "user")
212
+ rails generate pu:rodauth:account author
213
+ ```
214
+
215
+ Then configure each portal's controller concern:
216
+
217
+ ```ruby
218
+ # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
219
+ include Plutonium::Auth::Rodauth(:admin)
220
+
221
+ # packages/author_portal/app/controllers/author_portal/concerns/controller.rb
222
+ include Plutonium::Auth::Rodauth(:author)
223
+ ```
224
+
225
+ ## Session Configuration
226
+
227
+ Rodauth sessions can be configured in the Rodauth plugin:
228
+
229
+ ```ruby
230
+ # app/rodauth/admin_rodauth_plugin.rb
231
+ class AdminRodauthPlugin < RodauthPlugin
232
+ configure do
233
+ # Session expires after 30 days
234
+ session_expiration_seconds 30.days.to_i
235
+
236
+ # Require re-authentication for sensitive actions
237
+ password_grace_period 3600
238
+ end
239
+ end
240
+ ```
241
+
242
+ ## What's Next
243
+
244
+ Users can now log in, but anyone can do anything. In the next chapter, we'll implement authorization policies to control access.
245
+
246
+ [Continue to Chapter 4: Implementing Authorization →](./04-authorization)
@@ -0,0 +1,170 @@
1
+ # Chapter 4: Implementing Authorization
2
+
3
+ In this chapter, you'll implement authorization policies to control who can do what.
4
+
5
+ ## Understanding Policies
6
+
7
+ Plutonium uses policy classes to control authorization at three levels:
8
+
9
+ 1. **Action Permissions** - Can the user perform this action? (create, read, update, delete)
10
+ 2. **Attribute Permissions** - Which attributes can the user see/modify?
11
+ 3. **Scope Permissions** - Which records can the user access?
12
+
13
+ ## The Policy Class
14
+
15
+ Open the post policy:
16
+
17
+ ```ruby
18
+ # packages/blogging/app/policies/blogging/post_policy.rb
19
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
20
+ # By default, all actions are permitted
21
+ # Let's add some restrictions
22
+ end
23
+ ```
24
+
25
+ ## Action Permissions
26
+
27
+ Let's implement basic CRUD permissions:
28
+
29
+ ```ruby
30
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
31
+ # Anyone can view published posts
32
+ def read?
33
+ record.published? || owner?
34
+ end
35
+
36
+ # Only the owner can edit
37
+ def update?
38
+ owner?
39
+ end
40
+
41
+ # Only the owner can delete
42
+ def destroy?
43
+ owner?
44
+ end
45
+
46
+ # Anyone authenticated can create
47
+ def create?
48
+ true
49
+ end
50
+
51
+ private
52
+
53
+ def owner?
54
+ record.user_id == user.id
55
+ end
56
+ end
57
+ ```
58
+
59
+ ## Understanding the Policy Context
60
+
61
+ Inside a policy, you have access to:
62
+
63
+ | Accessor | Description |
64
+ |----------|-------------|
65
+ | `user` | The current authenticated user |
66
+ | `record` | The resource being authorized |
67
+ | `entity_scope` | Parent record for scoping (e.g., Organization in multi-tenant apps) |
68
+
69
+ ```ruby
70
+ def some_permission?
71
+ user # => Current user (from authentication)
72
+ record # => The Post instance being checked
73
+ entity_scope # => Parent record for multi-tenancy (or nil)
74
+ end
75
+ ```
76
+
77
+ ## Attribute Permissions
78
+
79
+ Control which fields users can view and modify:
80
+
81
+ ```ruby
82
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
83
+ # Attributes allowed in forms (create/update)
84
+ def permitted_attributes_for_create
85
+ [:title, :body, :user_id]
86
+ end
87
+
88
+ def permitted_attributes_for_update
89
+ if owner?
90
+ [:title, :body, :published]
91
+ else
92
+ [] # Non-owners can't edit
93
+ end
94
+ end
95
+
96
+ # Attributes visible in views
97
+ def permitted_attributes_for_read
98
+ if owner? || record.published?
99
+ [:title, :body, :published, :created_at, :user]
100
+ else
101
+ [:title] # Limited view for unpublished posts
102
+ end
103
+ end
104
+ end
105
+ ```
106
+
107
+ ## Scope Permissions
108
+
109
+ Control which records appear in listings:
110
+
111
+ ```ruby
112
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
113
+ # Called when listing posts
114
+ def relation_scope(relation)
115
+ if admin?
116
+ relation # Admins see everything
117
+ else
118
+ relation.where(published: true).or(relation.where(user_id: user.id))
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def admin?
125
+ user.respond_to?(:admin?) && user.admin?
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Portal-Specific Policies
131
+
132
+ Different portals can have different policies. Create a portal-specific policy:
133
+
134
+ ```ruby
135
+ # packages/admin_portal/app/policies/admin_portal/blogging/post_policy.rb
136
+ class AdminPortal::Blogging::PostPolicy < ::Blogging::PostPolicy
137
+ # Admins can do everything
138
+ def read?
139
+ true
140
+ end
141
+
142
+ def update?
143
+ true
144
+ end
145
+
146
+ def destroy?
147
+ true
148
+ end
149
+
150
+ def relation_scope(relation)
151
+ relation # No scope restrictions for admins
152
+ end
153
+ end
154
+ ```
155
+
156
+ Plutonium automatically uses the portal-specific policy when available.
157
+
158
+ ## Learn More
159
+
160
+ Plutonium policies extend [ActionPolicy](https://actionpolicy.evilmartians.io/). See the ActionPolicy documentation for advanced features like:
161
+
162
+ - [Aliases](https://actionpolicy.evilmartians.io/#/aliases) - Group actions under common rules
163
+ - [Pre-checks](https://actionpolicy.evilmartians.io/#/pre_checks) - Skip checks for certain users (e.g., admins)
164
+ - [Caching](https://actionpolicy.evilmartians.io/#/caching) - Cache authorization results
165
+
166
+ ## What's Next
167
+
168
+ We have CRUD working with proper authorization. In the next chapter, we'll add a custom "Publish" action using Interactions.
169
+
170
+ [Continue to Chapter 5: Adding Custom Actions →](./05-custom-actions)