plutonium 0.23.4 → 0.23.5
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/app/assets/plutonium.css +2 -2
- data/config/initializers/sqlite_json_alias.rb +1 -1
- data/docs/.vitepress/config.ts +60 -19
- data/docs/guide/cursor-rules.md +75 -0
- data/docs/guide/deep-dive/authorization.md +189 -0
- data/docs/guide/{getting-started → deep-dive}/resources.md +137 -0
- data/docs/guide/getting-started/{installation.md → 01-installation.md} +0 -105
- data/docs/guide/index.md +28 -0
- data/docs/guide/introduction/02-core-concepts.md +440 -0
- data/docs/guide/tutorial/01-project-setup.md +75 -0
- data/docs/guide/tutorial/02-creating-a-feature-package.md +45 -0
- data/docs/guide/tutorial/03-defining-resources.md +90 -0
- data/docs/guide/tutorial/04-creating-a-portal.md +101 -0
- data/docs/guide/tutorial/05-customizing-the-ui.md +128 -0
- data/docs/guide/tutorial/06-adding-custom-actions.md +101 -0
- data/docs/guide/tutorial/07-implementing-authorization.md +90 -0
- data/docs/index.md +24 -31
- data/docs/modules/action.md +190 -0
- data/docs/modules/authentication.md +236 -0
- data/docs/modules/configuration.md +599 -0
- data/docs/modules/controller.md +398 -0
- data/docs/modules/core.md +316 -0
- data/docs/modules/definition.md +876 -0
- data/docs/modules/display.md +759 -0
- data/docs/modules/form.md +605 -0
- data/docs/modules/generator.md +288 -0
- data/docs/modules/index.md +167 -0
- data/docs/modules/interaction.md +470 -0
- data/docs/modules/package.md +151 -0
- data/docs/modules/policy.md +176 -0
- data/docs/modules/portal.md +710 -0
- data/docs/modules/query.md +287 -0
- data/docs/modules/resource_record.md +618 -0
- data/docs/modules/routing.md +641 -0
- data/docs/modules/table.md +293 -0
- data/docs/modules/ui.md +631 -0
- data/docs/public/plutonium.mdc +667 -0
- data/lib/generators/pu/core/assets/assets_generator.rb +0 -5
- data/lib/plutonium/ui/display/resource.rb +7 -2
- data/lib/plutonium/ui/table/resource.rb +8 -3
- data/lib/plutonium/version.rb +1 -1
- metadata +36 -9
- data/docs/guide/getting-started/authorization.md +0 -296
- data/docs/guide/getting-started/core-concepts.md +0 -432
- data/docs/guide/getting-started/index.md +0 -21
- data/docs/guide/tutorial.md +0 -401
- /data/docs/guide/{what-is-plutonium.md → introduction/01-what-is-plutonium.md} +0 -0
@@ -0,0 +1,710 @@
|
|
1
|
+
---
|
2
|
+
title: Portal Module
|
3
|
+
---
|
4
|
+
|
5
|
+
# Portal Module
|
6
|
+
|
7
|
+
The Portal module is your key to building sophisticated, multi-tenant applications with distinct user experiences. Think of portals as separate "faces" of your application—each designed for different types of users, with their own authentication, styling, and access controls, while sharing the same underlying business logic.
|
8
|
+
|
9
|
+
::: tip
|
10
|
+
The Portal module is located in `lib/plutonium/portal/`. Portals are typically generated as packages in the `packages/` directory.
|
11
|
+
:::
|
12
|
+
|
13
|
+
## What Portals Solve
|
14
|
+
|
15
|
+
Modern applications often need to serve different types of users with completely different interfaces:
|
16
|
+
|
17
|
+
- **Admin Portal**: Full system access for administrators and staff
|
18
|
+
- **Customer Portal**: Self-service interface for customers and clients
|
19
|
+
- **Partner Portal**: Specialized access for business partners
|
20
|
+
- **Public Portal**: Public-facing content and marketing pages
|
21
|
+
|
22
|
+
Each portal can have its own authentication system, visual design, feature set, and data access patterns, while sharing the same core business logic and data models.
|
23
|
+
|
24
|
+
## Core Portal Capabilities
|
25
|
+
|
26
|
+
- **Application Segmentation**: Create completely isolated user experiences
|
27
|
+
- **Multi-Tenant Architecture**: Automatically scope data to organizations, accounts, or other entities
|
28
|
+
- **Independent Routing**: Each portal has its own URL structure and route namespace
|
29
|
+
- **Portal-Specific Authentication**: Different login systems and security requirements per portal
|
30
|
+
- **Flexible Access Control**: Fine-grained permissions tailored to each user type
|
31
|
+
|
32
|
+
## Creating a Portal
|
33
|
+
|
34
|
+
Portals are Rails Engines enhanced with Plutonium's portal functionality. The easiest way to create one is with the generator:
|
35
|
+
|
36
|
+
::: code-group
|
37
|
+
```bash [Generate a Portal]
|
38
|
+
rails generate pu:pkg:portal admin
|
39
|
+
```
|
40
|
+
|
41
|
+
```ruby [packages/admin_portal/lib/engine.rb]
|
42
|
+
module AdminPortal
|
43
|
+
class Engine < ::Rails::Engine
|
44
|
+
# This inclusion provides all portal functionality
|
45
|
+
include Plutonium::Portal::Engine
|
46
|
+
|
47
|
+
# Optional: Configure multi-tenancy
|
48
|
+
scope_to_entity Organization, strategy: :path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
```ruby [packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb]
|
54
|
+
# A base concern is created for portal-wide logic
|
55
|
+
module AdminPortal
|
56
|
+
module Concerns
|
57
|
+
module Controller
|
58
|
+
extend ActiveSupport::Concern
|
59
|
+
include Plutonium::Portal::Controller
|
60
|
+
|
61
|
+
# Include authentication specific to this portal
|
62
|
+
include Plutonium::Auth::Rodauth(:admin)
|
63
|
+
|
64
|
+
included do
|
65
|
+
# Add portal-specific logic
|
66
|
+
before_action :ensure_admin_access
|
67
|
+
layout "admin_portal"
|
68
|
+
|
69
|
+
# Portal-wide error handling
|
70
|
+
rescue_from AdminPortal::AccessDenied, with: :handle_access_denied
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def ensure_admin_access
|
76
|
+
redirect_to root_path, error: "Admin access required" unless current_user&.admin?
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_access_denied(exception)
|
80
|
+
redirect_to admin_root_path, error: "Access denied: #{exception.message}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
:::
|
87
|
+
|
88
|
+
## Multi-Tenancy with Entity Scoping
|
89
|
+
|
90
|
+
One of the most powerful features of portals is automatic multi-tenancy through entity scoping. When you scope a portal to an entity (like Organization or Account), all data access is automatically filtered and secured.
|
91
|
+
|
92
|
+
### Path-Based Scoping
|
93
|
+
|
94
|
+
The most straightforward approach uses URL parameters to identify the tenant:
|
95
|
+
|
96
|
+
::: code-group
|
97
|
+
```ruby [Engine Configuration]
|
98
|
+
# packages/admin_portal/lib/engine.rb
|
99
|
+
scope_to_entity Organization, strategy: :path
|
100
|
+
```
|
101
|
+
|
102
|
+
```ruby [Route Configuration]
|
103
|
+
# packages/admin_portal/config/routes.rb
|
104
|
+
AdminPortal::Engine.routes.draw do
|
105
|
+
# These routes are automatically nested under /organizations/:organization_id
|
106
|
+
register_resource Blog::Post
|
107
|
+
register_resource Blog::Comment
|
108
|
+
end
|
109
|
+
|
110
|
+
# Generated routes:
|
111
|
+
# GET /organizations/:organization_id/posts
|
112
|
+
# GET /organizations/:organization_id/posts/:id
|
113
|
+
# POST /organizations/:organization_id/posts
|
114
|
+
```
|
115
|
+
|
116
|
+
```ruby [Automatic Data Scoping]
|
117
|
+
# In any controller within the Admin Portal
|
118
|
+
class AdminPortal::PostsController < AdminPortal::ResourceController
|
119
|
+
def index
|
120
|
+
# current_scoped_entity returns the Organization from the URL
|
121
|
+
# All queries are automatically scoped to this organization
|
122
|
+
# @posts = current_scoped_entity.posts.authorized_scope(...)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
:::
|
127
|
+
|
128
|
+
### Custom Scoping Strategies
|
129
|
+
|
130
|
+
For more sophisticated multi-tenancy, implement custom scoping strategies:
|
131
|
+
|
132
|
+
::: code-group
|
133
|
+
```ruby [Engine Configuration]
|
134
|
+
# Use subdomain-based tenancy
|
135
|
+
scope_to_entity Account, strategy: :current_account
|
136
|
+
```
|
137
|
+
|
138
|
+
```ruby [Controller Implementation]
|
139
|
+
module CustomerPortal::Concerns::Controller
|
140
|
+
private
|
141
|
+
|
142
|
+
# Method name must match the strategy name exactly
|
143
|
+
def current_account
|
144
|
+
@current_account ||= Account.find_by!(subdomain: request.subdomain)
|
145
|
+
rescue ActiveRecord::RecordNotFound
|
146
|
+
redirect_to root_path, error: "Invalid account subdomain"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
:::
|
151
|
+
|
152
|
+
### Database Association Requirements
|
153
|
+
|
154
|
+
For automatic scoping to work, Plutonium needs to find a path from your resources to the scoping entity:
|
155
|
+
|
156
|
+
::: code-group
|
157
|
+
```ruby [Direct Association (Preferred)]
|
158
|
+
# Plutonium automatically finds this relationship
|
159
|
+
class Post < ApplicationRecord
|
160
|
+
belongs_to :organization
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
```ruby [Indirect Association]
|
165
|
+
# Plutonium can traverse one level of has_one or belongs_to
|
166
|
+
class Post < ApplicationRecord
|
167
|
+
belongs_to :author, class_name: 'User'
|
168
|
+
has_one :organization, through: :author
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
```ruby [Manual Scope (For Complex Cases)]
|
173
|
+
# Define a scope for complex relationships
|
174
|
+
class Comment < ApplicationRecord
|
175
|
+
belongs_to :post
|
176
|
+
|
177
|
+
scope :associated_with_organization, ->(organization) do
|
178
|
+
joins(post: :author).where(users: { organization_id: organization.id })
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
:::
|
183
|
+
|
184
|
+
## Portal Examples and Use Cases
|
185
|
+
|
186
|
+
### Admin Portal: Internal Management
|
187
|
+
|
188
|
+
Perfect for system administrators and internal staff who need full access:
|
189
|
+
|
190
|
+
::: code-group
|
191
|
+
```ruby [Configuration]
|
192
|
+
# packages/admin_portal/lib/engine.rb
|
193
|
+
scope_to_entity Organization, strategy: :path
|
194
|
+
|
195
|
+
# packages/admin_portal/config/routes.rb
|
196
|
+
register_resource User
|
197
|
+
register_resource Organization
|
198
|
+
register_resource Blog::Post
|
199
|
+
register_resource Analytics::Report
|
200
|
+
```
|
201
|
+
|
202
|
+
```ruby [Authentication & Authorization]
|
203
|
+
# packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
|
204
|
+
include Plutonium::Auth::Rodauth(:admin)
|
205
|
+
|
206
|
+
included do
|
207
|
+
before_action :require_admin_role
|
208
|
+
before_action :set_admin_context
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def require_admin_role
|
214
|
+
redirect_to root_path unless current_user&.admin?
|
215
|
+
end
|
216
|
+
```
|
217
|
+
:::
|
218
|
+
|
219
|
+
### Customer Portal: Self-Service Interface
|
220
|
+
|
221
|
+
Designed for customers to manage their own accounts and data:
|
222
|
+
|
223
|
+
::: code-group
|
224
|
+
```ruby [Configuration]
|
225
|
+
# packages/customer_portal/lib/engine.rb
|
226
|
+
scope_to_entity Organization, strategy: :current_organization
|
227
|
+
|
228
|
+
# packages/customer_portal/config/routes.rb
|
229
|
+
register_resource Project
|
230
|
+
register_resource Invoice
|
231
|
+
register_resource SupportTicket
|
232
|
+
```
|
233
|
+
|
234
|
+
```ruby [Custom Scoping]
|
235
|
+
# packages/customer_portal/app/controllers/customer_portal/concerns/controller.rb
|
236
|
+
include Plutonium::Auth::Rodauth(:customer)
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def current_organization
|
241
|
+
@current_organization ||= current_user&.organization
|
242
|
+
end
|
243
|
+
```
|
244
|
+
:::
|
245
|
+
|
246
|
+
### Public Portal: No Authentication Required
|
247
|
+
|
248
|
+
For marketing sites, blogs, and public content:
|
249
|
+
|
250
|
+
::: code-group
|
251
|
+
```ruby [Configuration]
|
252
|
+
# packages/public_portal/lib/engine.rb
|
253
|
+
# No scope_to_entity - public data
|
254
|
+
|
255
|
+
# packages/public_portal/config/routes.rb
|
256
|
+
register_resource Blog::Post
|
257
|
+
register_resource Page
|
258
|
+
register_resource ContactForm
|
259
|
+
```
|
260
|
+
|
261
|
+
```ruby [Public Access]
|
262
|
+
# packages/public_portal/app/controllers/public_portal/concerns/controller.rb
|
263
|
+
# No authentication required
|
264
|
+
include Plutonium::Portal::Controller
|
265
|
+
|
266
|
+
# Custom public-specific logic
|
267
|
+
before_action :track_visitor_analytics
|
268
|
+
```
|
269
|
+
:::
|
270
|
+
|
271
|
+
## Authentication Integration
|
272
|
+
|
273
|
+
### Rodauth Multi-Account Setup
|
274
|
+
|
275
|
+
Portals integrate seamlessly with Rodauth for sophisticated authentication:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
# config/rodauth.rb
|
279
|
+
class RodauthApp < Roda
|
280
|
+
plugin :rodauth, json: :only do
|
281
|
+
# Admin authentication
|
282
|
+
rodauth :admin do
|
283
|
+
enable :login, :logout, :create_account, :verify_account,
|
284
|
+
:reset_password, :change_password, :otp, :recovery_codes
|
285
|
+
|
286
|
+
rails_account_model { Admin }
|
287
|
+
rails_controller { Rodauth::AdminController }
|
288
|
+
prefix "/admin/auth"
|
289
|
+
|
290
|
+
# Require MFA for admin accounts
|
291
|
+
two_factor_auth_required? true
|
292
|
+
end
|
293
|
+
|
294
|
+
# Customer authentication with user-friendly features
|
295
|
+
rodauth :customer do
|
296
|
+
enable :login, :logout, :create_account, :verify_account,
|
297
|
+
:reset_password, :change_password, :remember
|
298
|
+
|
299
|
+
rails_account_model { Customer }
|
300
|
+
rails_controller { Rodauth::CustomerController }
|
301
|
+
prefix "/auth"
|
302
|
+
|
303
|
+
# Remember me functionality
|
304
|
+
remember_deadline 30.days
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
```
|
309
|
+
|
310
|
+
### Portal-Specific Authentication
|
311
|
+
|
312
|
+
Each portal includes its appropriate authentication:
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
# Admin Portal - High security
|
316
|
+
module AdminPortal
|
317
|
+
module Concerns
|
318
|
+
module Controller
|
319
|
+
include Plutonium::Auth::Rodauth(:admin)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Customer Portal - User-friendly
|
325
|
+
module CustomerPortal
|
326
|
+
module Concerns
|
327
|
+
module Controller
|
328
|
+
include Plutonium::Auth::Rodauth(:customer)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
### Route-Level Authentication
|
335
|
+
|
336
|
+
Enforce authentication at the routing level:
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
# config/routes.rb
|
340
|
+
Rails.application.routes.draw do
|
341
|
+
# Admin portal requires admin authentication
|
342
|
+
constraints Rodauth::Rails.authenticate(:admin) do
|
343
|
+
mount AdminPortal::Engine, at: "/admin"
|
344
|
+
end
|
345
|
+
|
346
|
+
# Customer portal requires customer authentication
|
347
|
+
constraints Rodauth::Rails.authenticate(:customer) do
|
348
|
+
mount CustomerPortal::Engine, at: "/app"
|
349
|
+
end
|
350
|
+
|
351
|
+
# Public portal has no authentication constraint
|
352
|
+
mount PublicPortal::Engine, at: "/"
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
## Resource Management and Access Control
|
357
|
+
|
358
|
+
### Resource Registration
|
359
|
+
|
360
|
+
Resources must be explicitly registered with each portal:
|
361
|
+
|
362
|
+
```ruby
|
363
|
+
# Admin portal - comprehensive access
|
364
|
+
AdminPortal::Engine.routes.draw do
|
365
|
+
register_resource User
|
366
|
+
register_resource Organization
|
367
|
+
register_resource Blog::Post
|
368
|
+
register_resource Blog::Comment
|
369
|
+
register_resource Analytics::Report
|
370
|
+
register_resource Billing::Invoice
|
371
|
+
end
|
372
|
+
|
373
|
+
# Customer portal - limited, relevant resources
|
374
|
+
CustomerPortal::Engine.routes.draw do
|
375
|
+
register_resource Project
|
376
|
+
register_resource Billing::Invoice # Access controlled via policy
|
377
|
+
register_resource SupportTicket
|
378
|
+
end
|
379
|
+
|
380
|
+
# Public portal - read-only, published content
|
381
|
+
PublicPortal::Engine.routes.draw do
|
382
|
+
register_resource Blog::Post # Only published posts via policy
|
383
|
+
register_resource Page # Only public pages via policy
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
### Conditional Resource Registration
|
388
|
+
|
389
|
+
Dynamically register resources based on configuration or environment:
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
AdminPortal::Engine.routes.draw do
|
393
|
+
register_resource User
|
394
|
+
register_resource Organization
|
395
|
+
|
396
|
+
# Feature flags
|
397
|
+
register_resource Blog::Post if Rails.application.config.enable_blog
|
398
|
+
register_resource Analytics::Report if Rails.application.config.enable_analytics
|
399
|
+
|
400
|
+
# Environment-specific resources
|
401
|
+
register_resource SystemLog if Rails.env.development?
|
402
|
+
register_resource PerformanceMetric if Rails.env.production?
|
403
|
+
end
|
404
|
+
```
|
405
|
+
|
406
|
+
### Portal-Specific Access Control
|
407
|
+
|
408
|
+
Since `register_resource` doesn't support Rails' `only:` and `except:` options, access control is handled through portal-specific policies:
|
409
|
+
|
410
|
+
```ruby
|
411
|
+
# Customer portal - read-only invoice access
|
412
|
+
class CustomerPortal::Billing::InvoicePolicy < Plutonium::Resource::Policy
|
413
|
+
def create?
|
414
|
+
false # Customers can't create invoices
|
415
|
+
end
|
416
|
+
|
417
|
+
def update?
|
418
|
+
false # Customers can't modify invoices
|
419
|
+
end
|
420
|
+
|
421
|
+
def destroy?
|
422
|
+
false # Customers can't delete invoices
|
423
|
+
end
|
424
|
+
|
425
|
+
def read?
|
426
|
+
record.organization == user.organization # Only their org's invoices
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# Public portal - only published content
|
431
|
+
class PublicPortal::Blog::PostPolicy < Plutonium::Resource::Policy
|
432
|
+
def create?
|
433
|
+
false # No creation in public portal
|
434
|
+
end
|
435
|
+
|
436
|
+
def update?
|
437
|
+
false # No editing in public portal
|
438
|
+
end
|
439
|
+
|
440
|
+
def destroy?
|
441
|
+
false # No deletion in public portal
|
442
|
+
end
|
443
|
+
|
444
|
+
def read?
|
445
|
+
record.published? && record.public? # Only published, public posts
|
446
|
+
end
|
447
|
+
end
|
448
|
+
```
|
449
|
+
|
450
|
+
### Portal-Specific Policy Inheritance
|
451
|
+
|
452
|
+
Create portal-specific policy variations:
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
# Admin portal - enhanced permissions for admins
|
456
|
+
module AdminPortal
|
457
|
+
class UserPolicy < ::UserPolicy
|
458
|
+
def create?
|
459
|
+
user.super_admin? # Only super admins can create users
|
460
|
+
end
|
461
|
+
|
462
|
+
def destroy?
|
463
|
+
user.super_admin? && record != user # Can't delete themselves
|
464
|
+
end
|
465
|
+
|
466
|
+
def impersonate?
|
467
|
+
user.super_admin? && Rails.env.development?
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Customer portal - restricted permissions
|
473
|
+
module CustomerPortal
|
474
|
+
class ProjectPolicy < ::ProjectPolicy
|
475
|
+
def index?
|
476
|
+
true # Can list their projects
|
477
|
+
end
|
478
|
+
|
479
|
+
def show?
|
480
|
+
record.organization == user.organization # Only their org's projects
|
481
|
+
end
|
482
|
+
|
483
|
+
def create?
|
484
|
+
user.can_create_projects? && user.organization.active?
|
485
|
+
end
|
486
|
+
|
487
|
+
def destroy?
|
488
|
+
false # Customers can't delete projects
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
```
|
493
|
+
|
494
|
+
## Advanced Portal Customization
|
495
|
+
|
496
|
+
### Portal-Specific Layouts and Styling
|
497
|
+
|
498
|
+
Each portal can have completely different visual designs:
|
499
|
+
|
500
|
+
```erb
|
501
|
+
<!-- packages/admin_portal/app/views/layouts/admin_portal.html.erb -->
|
502
|
+
<!DOCTYPE html>
|
503
|
+
<html>
|
504
|
+
<head>
|
505
|
+
<title>Admin Portal - <%= @page_title || "Dashboard" %></title>
|
506
|
+
<%= csrf_meta_tags %>
|
507
|
+
<%= csp_meta_tag %>
|
508
|
+
|
509
|
+
<%= stylesheet_link_tag "admin_portal", "data-turbo-track": "reload" %>
|
510
|
+
<%= javascript_include_tag "admin_portal", "data-turbo-track": "reload", defer: true %>
|
511
|
+
</head>
|
512
|
+
|
513
|
+
<body class="admin-theme dark-mode">
|
514
|
+
<!-- Admin-specific navigation -->
|
515
|
+
<nav class="admin-nav">
|
516
|
+
<%= link_to "Dashboard", admin_portal.root_path, class: "nav-link" %>
|
517
|
+
<%= link_to "Users", admin_portal.users_path, class: "nav-link" %>
|
518
|
+
<%= link_to "Organizations", admin_portal.organizations_path, class: "nav-link" %>
|
519
|
+
|
520
|
+
<div class="nav-user">
|
521
|
+
<%= current_user.name %>
|
522
|
+
<%= link_to "Logout", admin_portal.logout_path, method: :delete %>
|
523
|
+
</div>
|
524
|
+
</nav>
|
525
|
+
|
526
|
+
<main class="admin-content">
|
527
|
+
<!-- Flash messages with admin styling -->
|
528
|
+
<% flash.each do |type, message| %>
|
529
|
+
<div class="alert alert-<%= type %> admin-alert">
|
530
|
+
<%= message %>
|
531
|
+
</div>
|
532
|
+
<% end %>
|
533
|
+
|
534
|
+
<%= yield %>
|
535
|
+
</main>
|
536
|
+
</body>
|
537
|
+
</html>
|
538
|
+
```
|
539
|
+
|
540
|
+
### Portal-Specific Components
|
541
|
+
|
542
|
+
Create reusable components tailored to each portal:
|
543
|
+
|
544
|
+
```ruby
|
545
|
+
# packages/admin_portal/app/components/admin_portal/sidebar_component.rb
|
546
|
+
module AdminPortal
|
547
|
+
class SidebarComponent < Plutonium::UI::Component::Base
|
548
|
+
def view_template
|
549
|
+
aside(class: "admin-sidebar") do
|
550
|
+
nav do
|
551
|
+
ul(class: "nav-menu") do
|
552
|
+
li { link_to "Dashboard", root_path, class: nav_link_class("dashboard") }
|
553
|
+
li { link_to "Users", users_path, class: nav_link_class("users") }
|
554
|
+
li { link_to "Organizations", organizations_path, class: nav_link_class("organizations") }
|
555
|
+
|
556
|
+
# Conditional navigation based on permissions
|
557
|
+
if current_user.super_admin?
|
558
|
+
li { link_to "System Logs", system_logs_path, class: nav_link_class("logs") }
|
559
|
+
li { link_to "Analytics", analytics_path, class: nav_link_class("analytics") }
|
560
|
+
end
|
561
|
+
|
562
|
+
# Feature-flagged navigation
|
563
|
+
if FeatureFlag.enabled?(:billing_portal)
|
564
|
+
li { link_to "Billing", billing_path, class: nav_link_class("billing") }
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
private
|
572
|
+
|
573
|
+
def nav_link_class(section)
|
574
|
+
base_class = "nav-link"
|
575
|
+
base_class += " active" if current_section == section
|
576
|
+
base_class
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
580
|
+
```
|
581
|
+
|
582
|
+
## Portal Generation and Setup
|
583
|
+
|
584
|
+
### Using Generators
|
585
|
+
|
586
|
+
Plutonium provides comprehensive generators for portal creation:
|
587
|
+
|
588
|
+
```bash
|
589
|
+
# Generate a full-featured admin portal
|
590
|
+
rails generate pu:pkg:portal admin
|
591
|
+
|
592
|
+
# Generate a customer portal
|
593
|
+
rails generate pu:pkg:portal customer
|
594
|
+
|
595
|
+
# Generate a public portal
|
596
|
+
rails generate pu:pkg:portal public
|
597
|
+
|
598
|
+
# Connect existing resources to portals
|
599
|
+
rails generate pu:res:conn post --dest=admin_portal
|
600
|
+
rails generate pu:res:conn project --dest=customer_portal
|
601
|
+
```
|
602
|
+
|
603
|
+
### Generated Portal Structure
|
604
|
+
|
605
|
+
Generators create a well-organized portal structure:
|
606
|
+
|
607
|
+
```
|
608
|
+
packages/admin_portal/
|
609
|
+
├── app/
|
610
|
+
│ ├── controllers/
|
611
|
+
│ │ └── admin_portal/
|
612
|
+
│ │ ├── concerns/
|
613
|
+
│ │ │ └── controller.rb # Portal-wide controller logic
|
614
|
+
│ │ ├── dashboard_controller.rb # Portal dashboard
|
615
|
+
│ │ ├── plutonium_controller.rb # Base controller
|
616
|
+
│ │ └── resource_controller.rb # Resource controller base
|
617
|
+
│ ├── policies/
|
618
|
+
│ │ └── admin_portal/ # Portal-specific policies
|
619
|
+
│ ├── definitions/
|
620
|
+
│ │ └── admin_portal/ # Portal-specific resource definitions
|
621
|
+
│ └── views/
|
622
|
+
│ └── layouts/
|
623
|
+
│ └── admin_portal.html.erb # Portal-specific layout
|
624
|
+
├── config/
|
625
|
+
│ └── routes.rb # Portal routes
|
626
|
+
└── lib/
|
627
|
+
└── engine.rb # Portal engine configuration
|
628
|
+
```
|
629
|
+
|
630
|
+
## Best Practices
|
631
|
+
|
632
|
+
### Multi-Tenancy Best Practices
|
633
|
+
|
634
|
+
**Entity Modeling**
|
635
|
+
Design clear entity relationships:
|
636
|
+
|
637
|
+
```ruby
|
638
|
+
# ✅ Good - clear entity hierarchy
|
639
|
+
class Organization < ApplicationRecord
|
640
|
+
has_many :users
|
641
|
+
has_many :projects
|
642
|
+
has_many :invoices
|
643
|
+
end
|
644
|
+
|
645
|
+
class User < ApplicationRecord
|
646
|
+
belongs_to :organization
|
647
|
+
has_many :projects
|
648
|
+
end
|
649
|
+
|
650
|
+
class Project < ApplicationRecord
|
651
|
+
belongs_to :organization
|
652
|
+
belongs_to :user
|
653
|
+
end
|
654
|
+
```
|
655
|
+
|
656
|
+
**Consistent Scoping**
|
657
|
+
Use the same scoping strategy throughout your portal:
|
658
|
+
|
659
|
+
```ruby
|
660
|
+
# ✅ Good - consistent scoping
|
661
|
+
class AdminPortal::Engine < Rails::Engine
|
662
|
+
scope_to_entity Organization, strategy: :path
|
663
|
+
end
|
664
|
+
|
665
|
+
# All controllers automatically scope to organization
|
666
|
+
# All policies receive the scoped organization context
|
667
|
+
```
|
668
|
+
|
669
|
+
### Security First
|
670
|
+
|
671
|
+
**Portal-Specific Authentication**
|
672
|
+
Use appropriate authentication for each portal:
|
673
|
+
|
674
|
+
```ruby
|
675
|
+
# ✅ Good - tailored authentication
|
676
|
+
module AdminPortal::Concerns::Controller
|
677
|
+
include Plutonium::Auth::Rodauth(:admin)
|
678
|
+
end
|
679
|
+
|
680
|
+
module CustomerPortal::Concerns::Controller
|
681
|
+
include Plutonium::Auth::Rodauth(:customer)
|
682
|
+
end
|
683
|
+
```
|
684
|
+
|
685
|
+
**Route Constraints**
|
686
|
+
Enforce authentication at the routing level:
|
687
|
+
|
688
|
+
```ruby
|
689
|
+
# ✅ Good - route-level security
|
690
|
+
Rails.application.routes.draw do
|
691
|
+
constraints Rodauth::Rails.authenticate(:admin) do
|
692
|
+
mount AdminPortal::Engine, at: "/admin"
|
693
|
+
end
|
694
|
+
|
695
|
+
constraints Rodauth::Rails.authenticate(:customer) do
|
696
|
+
mount CustomerPortal::Engine, at: "/app"
|
697
|
+
end
|
698
|
+
end
|
699
|
+
```
|
700
|
+
|
701
|
+
## Integration with Other Modules
|
702
|
+
|
703
|
+
The Portal module works seamlessly with other Plutonium components:
|
704
|
+
|
705
|
+
- **[Core](./core.md)**: Provides base controller functionality and entity scoping capabilities
|
706
|
+
- **[Authentication](./authentication.md)**: Portal-specific authentication strategies and session management
|
707
|
+
- **[Policy](./policy.md)**: Entity-aware authorization and portal-specific access control
|
708
|
+
- **[Package](./package.md)**: Package-based organization and resource registration
|
709
|
+
- **[Resource Record](./resource_record.md)**: Resource controllers work seamlessly within portal contexts
|
710
|
+
- **[Routing](./routing.md)**: Automatic route generation with entity scoping and portal isolation
|