agentcode 0.9.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +59 -0
  3. data/lib/agentcode/blueprint/blueprint_parser.rb +198 -0
  4. data/lib/agentcode/blueprint/blueprint_validator.rb +209 -0
  5. data/lib/agentcode/blueprint/generators/factory_generator.rb +74 -0
  6. data/lib/agentcode/blueprint/generators/policy_generator.rb +154 -0
  7. data/lib/agentcode/blueprint/generators/seeder_generator.rb +160 -0
  8. data/lib/agentcode/blueprint/generators/test_generator.rb +291 -0
  9. data/lib/agentcode/blueprint/manifest_manager.rb +81 -0
  10. data/lib/agentcode/commands/base_command.rb +57 -0
  11. data/lib/agentcode/commands/blueprint_command.rb +549 -0
  12. data/lib/agentcode/commands/export_postman_command.rb +328 -0
  13. data/lib/agentcode/commands/generate_command.rb +563 -0
  14. data/lib/agentcode/commands/install_command.rb +441 -0
  15. data/lib/agentcode/commands/invitation_link_command.rb +107 -0
  16. data/lib/agentcode/concerns/belongs_to_organization.rb +49 -0
  17. data/lib/agentcode/concerns/has_agentcode.rb +93 -0
  18. data/lib/agentcode/concerns/has_audit_trail.rb +125 -0
  19. data/lib/agentcode/concerns/has_auto_scope.rb +91 -0
  20. data/lib/agentcode/concerns/has_permissions.rb +117 -0
  21. data/lib/agentcode/concerns/has_uuid.rb +26 -0
  22. data/lib/agentcode/concerns/has_validation.rb +250 -0
  23. data/lib/agentcode/concerns/hidable_columns.rb +180 -0
  24. data/lib/agentcode/configuration.rb +98 -0
  25. data/lib/agentcode/controllers/auth_controller.rb +242 -0
  26. data/lib/agentcode/controllers/invitations_controller.rb +231 -0
  27. data/lib/agentcode/controllers/resources_controller.rb +813 -0
  28. data/lib/agentcode/engine.rb +65 -0
  29. data/lib/agentcode/mailers/invitation_mailer.rb +22 -0
  30. data/lib/agentcode/middleware/resolve_organization_from_route.rb +72 -0
  31. data/lib/agentcode/models/agentcode_model.rb +387 -0
  32. data/lib/agentcode/models/audit_log.rb +17 -0
  33. data/lib/agentcode/models/organization_invitation.rb +57 -0
  34. data/lib/agentcode/policies/invitation_policy.rb +54 -0
  35. data/lib/agentcode/policies/resource_policy.rb +197 -0
  36. data/lib/agentcode/query_builder.rb +278 -0
  37. data/lib/agentcode/railtie.rb +11 -0
  38. data/lib/agentcode/resource_scope.rb +59 -0
  39. data/lib/agentcode/routes.rb +124 -0
  40. data/lib/agentcode/tasks/agentcode.rake +39 -0
  41. data/lib/agentcode/templates/agentcode.rb +71 -0
  42. data/lib/agentcode/templates/agentcode_model.rb +104 -0
  43. data/lib/agentcode/templates/audit_trail/create_audit_logs.rb.erb +26 -0
  44. data/lib/agentcode/templates/generate/factory.rb.erb +43 -0
  45. data/lib/agentcode/templates/generate/migration.rb.erb +26 -0
  46. data/lib/agentcode/templates/generate/model.rb.erb +55 -0
  47. data/lib/agentcode/templates/generate/policy.rb.erb +52 -0
  48. data/lib/agentcode/templates/generate/scope.rb.erb +31 -0
  49. data/lib/agentcode/templates/multi_tenant/factories/organizations.rb.erb +9 -0
  50. data/lib/agentcode/templates/multi_tenant/factories/roles.rb.erb +9 -0
  51. data/lib/agentcode/templates/multi_tenant/factories/user_roles.rb.erb +10 -0
  52. data/lib/agentcode/templates/multi_tenant/factories/users.rb.erb +9 -0
  53. data/lib/agentcode/templates/multi_tenant/migrations/create_organizations.rb.erb +15 -0
  54. data/lib/agentcode/templates/multi_tenant/migrations/create_roles.rb.erb +15 -0
  55. data/lib/agentcode/templates/multi_tenant/migrations/create_user_roles.rb.erb +16 -0
  56. data/lib/agentcode/templates/multi_tenant/migrations/create_users.rb.erb +15 -0
  57. data/lib/agentcode/templates/multi_tenant/models/organization.rb.erb +18 -0
  58. data/lib/agentcode/templates/multi_tenant/models/role.rb.erb +11 -0
  59. data/lib/agentcode/templates/multi_tenant/models/user.rb.erb +14 -0
  60. data/lib/agentcode/templates/multi_tenant/models/user_role.rb.erb +9 -0
  61. data/lib/agentcode/templates/multi_tenant/policies/organization_policy.rb.erb +6 -0
  62. data/lib/agentcode/templates/multi_tenant/policies/role_policy.rb.erb +6 -0
  63. data/lib/agentcode/templates/multi_tenant/seeders/organization_seeder.rb.erb +9 -0
  64. data/lib/agentcode/templates/multi_tenant/seeders/role_seeder.rb.erb +19 -0
  65. data/lib/agentcode/templates/routes.rb +13 -0
  66. data/lib/agentcode/version.rb +5 -0
  67. data/lib/agentcode/views/lumina/invitation_mailer/invite.html.erb +29 -0
  68. data/lib/agentcode-rails.rb +3 -0
  69. data/lib/agentcode.rb +26 -0
  70. metadata +281 -0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pundit"
4
+ require "pagy"
5
+ require "discard"
6
+
7
+ module AgentCode
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace AgentCode
10
+
11
+ rake_tasks do
12
+ load File.expand_path("tasks/agentcode.rake", __dir__)
13
+ end
14
+
15
+ initializer "agentcode.autoloads" do
16
+ # Concerns
17
+ require "agentcode/concerns/has_agentcode"
18
+ require "agentcode/concerns/has_validation"
19
+ require "agentcode/concerns/has_permissions"
20
+ require "agentcode/concerns/has_audit_trail"
21
+ require "agentcode/concerns/belongs_to_organization"
22
+ require "agentcode/concerns/hidable_columns"
23
+ require "agentcode/concerns/has_uuid"
24
+ require "agentcode/concerns/has_auto_scope"
25
+
26
+ # Policies
27
+ require "agentcode/policies/resource_policy"
28
+ require "agentcode/policies/invitation_policy"
29
+
30
+ # Query builder and routes
31
+ require "agentcode/query_builder"
32
+ require "agentcode/routes"
33
+
34
+ # Controllers
35
+ require "agentcode/controllers/resources_controller"
36
+ require "agentcode/controllers/auth_controller"
37
+ require "agentcode/controllers/invitations_controller"
38
+
39
+ # Mailers (only if ActionMailer is available)
40
+ require "agentcode/mailers/invitation_mailer" if defined?(ActionMailer)
41
+ end
42
+
43
+ # Models that inherit from ApplicationRecord must be loaded after
44
+ # the app has initialized (so ApplicationRecord is defined)
45
+ initializer "agentcode.models", after: :load_active_record do
46
+ config.after_initialize do
47
+ require "agentcode/models/agentcode_model"
48
+ require "agentcode/models/audit_log"
49
+ require "agentcode/models/organization_invitation"
50
+ end
51
+ end
52
+
53
+ initializer "agentcode.routes", after: :load_config_initializers do |app|
54
+ app.routes.append do
55
+ AgentCode::Routes.draw(self)
56
+ end
57
+ end
58
+
59
+ initializer "agentcode.pundit" do
60
+ ActiveSupport.on_load(:action_controller) do
61
+ include Pundit::Authorization if defined?(Pundit)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # ActionMailer for invitation emails — mirrors Laravel InvitationNotification.
5
+ class InvitationMailer < ActionMailer::Base
6
+ def invite(invitation)
7
+ @invitation = invitation
8
+ @organization = invitation.organization
9
+ @role = invitation.role
10
+ @invited_by = invitation.inviter
11
+
12
+ frontend_url = ENV.fetch("FRONTEND_URL", "http://localhost:5173")
13
+ @url = "#{frontend_url}/accept-invitation?token=#{invitation.token}"
14
+ @expires_at = invitation.expires_at
15
+
16
+ mail(
17
+ to: invitation.email,
18
+ subject: "You've been invited to join #{@organization&.name}"
19
+ )
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ module Middleware
5
+ # Rack middleware that extracts the organization from the route parameter.
6
+ # Mirrors the Laravel ResolveOrganizationFromRoute middleware.
7
+ #
8
+ # For route-prefix multi-tenancy: /api/{organization}/posts
9
+ class ResolveOrganizationFromRoute
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ request = ActionDispatch::Request.new(env)
16
+
17
+ # Extract organization identifier from route params
18
+ org_identifier = request.path_parameters[:organization]
19
+
20
+ if org_identifier.present?
21
+ organization = find_organization(org_identifier)
22
+
23
+ unless organization
24
+ return [404, { "Content-Type" => "application/json" }, ['{"message":"Organization not found"}']]
25
+ end
26
+
27
+ # Check if authenticated user belongs to this organization
28
+ user = resolve_user(request)
29
+ if user && !user_belongs_to_organization?(user, organization)
30
+ return [404, { "Content-Type" => "application/json" }, ['{"message":"Organization not found"}']]
31
+ end
32
+
33
+ env["agentcode.organization"] = organization
34
+
35
+ if defined?(RequestStore)
36
+ RequestStore.store[:agentcode_organization] = organization
37
+ end
38
+ end
39
+
40
+ @app.call(env)
41
+ end
42
+
43
+ private
44
+
45
+ def find_organization(identifier)
46
+ org_class = "Organization".safe_constantize
47
+ return nil unless org_class
48
+
49
+ column = AgentCode.config.multi_tenant[:organization_identifier_column] || "id"
50
+ org_class.find_by(column => identifier)
51
+ end
52
+
53
+ def resolve_user(request)
54
+ token = request.headers["Authorization"]&.sub(/\ABearer /, "")
55
+ return nil unless token
56
+
57
+ user_class = "User".safe_constantize
58
+ return nil unless user_class
59
+
60
+ if user_class.column_names.include?("api_token")
61
+ user_class.find_by(api_token: token)
62
+ end
63
+ end
64
+
65
+ def user_belongs_to_organization?(user, organization)
66
+ return true unless user.respond_to?(:user_roles)
67
+
68
+ user.user_roles.exists?(organization_id: organization.id)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,387 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # AgentCodeModel -- Pre-composed base class for AgentCode-powered ActiveRecord models.
5
+ #
6
+ # Extends +ApplicationRecord+ and includes the most commonly needed concerns
7
+ # for AgentCode's automatic REST API generation. Subclass this instead of
8
+ # +ApplicationRecord+ to get query building, validation, column hiding,
9
+ # and auto-scopes out of the box.
10
+ #
11
+ # == Quick Start
12
+ #
13
+ # class Post < AgentCode::AgentCodeModel
14
+ # agentcode_filters :status, :user_id
15
+ # agentcode_sorts :created_at, :title
16
+ # agentcode_default_sort '-created_at'
17
+ # agentcode_includes :user, :comments
18
+ # agentcode_search :title, :content
19
+ #
20
+ # # Standard Rails validations for type/format (NOT presence — use allow_nil: true)
21
+ # validates :title, length: { maximum: 255 }, allow_nil: true
22
+ # validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
23
+ #
24
+ # # Field permissions are controlled by the policy (PostPolicy).
25
+ # # See: permitted_attributes_for_create / permitted_attributes_for_update
26
+ #
27
+ # belongs_to :user
28
+ # has_many :comments
29
+ # end
30
+ #
31
+ # == Included Concerns
32
+ #
33
+ # Concern | Purpose
34
+ # ------------------|-----------------------------------------------------------
35
+ # HasAgentCode | Query builder DSL (filters, sorts, includes, etc.)
36
+ # HasValidation | Format validation for request data
37
+ # HidableColumns | Dynamic column hiding from API responses
38
+ # HasAutoScope | Auto-discovery of ModelScopes::{Model}Scope classes
39
+ #
40
+ # == Optional Concerns (add manually when needed)
41
+ #
42
+ # These concerns are NOT included in AgentCodeModel because they require
43
+ # additional database columns, gems, or relationships. Include them in
44
+ # your model subclass as needed:
45
+ #
46
+ # Concern | Purpose
47
+ # ----------------------------|---------------------------------------------------
48
+ # AgentCode::HasAuditTrail | Automatic change logging to +audit_logs+ table
49
+ # AgentCode::HasUuid | Auto-generated UUID on creation
50
+ # AgentCode::BelongsToOrganization | Multi-tenant organization scoping
51
+ # AgentCode::HasPermissions | Permission checking (User model only)
52
+ # Discard::Model | Soft deletes via the Discard gem
53
+ #
54
+ # class Invoice < AgentCode::AgentCodeModel
55
+ # include AgentCode::HasAuditTrail
56
+ # include AgentCode::BelongsToOrganization
57
+ # include Discard::Model
58
+ #
59
+ # agentcode_filters :status, :client_id
60
+ # agentcode_sorts :created_at, :amount
61
+ #
62
+ # validates :amount, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
63
+ # validates :client_id, numericality: { only_integer: true }, allow_nil: true
64
+ #
65
+ # end
66
+ #
67
+ # @see AgentCode::HasAgentCode Query builder configuration
68
+ # @see AgentCode::HasValidation Format validation
69
+ # @see AgentCode::HidableColumns Column visibility control
70
+ # @see AgentCode::HasAutoScope Automatic scope discovery
71
+ #
72
+ class AgentCodeModel < ::ApplicationRecord
73
+ self.abstract_class = true
74
+
75
+ include AgentCode::HasAgentCode
76
+ include AgentCode::HasValidation
77
+ include AgentCode::HidableColumns
78
+ include AgentCode::HasAutoScope
79
+
80
+ # =========================================================================
81
+ # QUERY BUILDER -- Filtering, Sorting, Search, Includes, Fields
82
+ # =========================================================================
83
+ # Provided by: AgentCode::HasAgentCode
84
+ #
85
+ # All class_attributes below are set via DSL methods. You can also
86
+ # override them directly using +self.attribute_name = value+ in the
87
+ # class body if you prefer a declarative style.
88
+ # =========================================================================
89
+
90
+ # @!attribute [rw] allowed_filters
91
+ # Filterable columns.
92
+ #
93
+ # Controls which fields can be filtered via +?filter[field]=value+.
94
+ # Only whitelisted fields are accepted -- unlisted fields are silently ignored.
95
+ #
96
+ # Set via DSL: +agentcode_filters :status, :user_id, :category_id+
97
+ #
98
+ # Query: +GET /api/posts?filter[status]=published&filter[user_id]=5+
99
+ #
100
+ # @return [Array<String>]
101
+ # @example
102
+ # agentcode_filters :status, :user_id, :category_id, :is_published
103
+ # @example Direct assignment
104
+ # self.allowed_filters = %w[status user_id category_id]
105
+ self.allowed_filters = []
106
+
107
+ # @!attribute [rw] allowed_sorts
108
+ # Sortable columns.
109
+ #
110
+ # Controls which fields can be used for sorting via +?sort=field+.
111
+ # Prefix with +-+ for descending order.
112
+ #
113
+ # Set via DSL: +agentcode_sorts :created_at, :title, :status+
114
+ #
115
+ # Query: +GET /api/posts?sort=-created_at+ or +GET /api/posts?sort=title+
116
+ #
117
+ # @return [Array<String>]
118
+ # @example
119
+ # agentcode_sorts :created_at, :title, :status, :updated_at
120
+ self.allowed_sorts = []
121
+
122
+ # @!attribute [rw] default_sort_field
123
+ # Default sort expression applied when no explicit +?sort+ is given.
124
+ # Prefix with +-+ for descending. Set to +nil+ for database insertion order.
125
+ #
126
+ # Set via DSL: +agentcode_default_sort '-created_at'+
127
+ #
128
+ # @return [String, nil]
129
+ # @example
130
+ # agentcode_default_sort '-created_at' # newest first
131
+ # agentcode_default_sort 'title' # alphabetical ascending
132
+ self.default_sort_field = nil
133
+
134
+ # @!attribute [rw] allowed_fields
135
+ # Selectable columns (sparse fieldsets).
136
+ #
137
+ # Controls which columns can be selected via +?fields[model]=field1,field2+.
138
+ # Limits the payload size by returning only requested columns.
139
+ #
140
+ # Set via DSL: +agentcode_fields :id, :title, :status, :created_at+
141
+ #
142
+ # Query: +GET /api/posts?fields[posts]=id,title,status+
143
+ #
144
+ # @return [Array<String>]
145
+ # @example
146
+ # agentcode_fields :id, :title, :status, :created_at, :user_id
147
+ self.allowed_fields = []
148
+
149
+ # @!attribute [rw] allowed_includes
150
+ # Eager-loadable relationships.
151
+ #
152
+ # Controls which relationships can be included via +?include=relation+.
153
+ # Must correspond to defined ActiveRecord associations on the model.
154
+ # Supports nested includes: +'comments.user'+.
155
+ #
156
+ # Set via DSL: +agentcode_includes :user, :comments, :tags+
157
+ #
158
+ # Query: +GET /api/posts?include=user,comments+
159
+ #
160
+ # @return [Array<String>]
161
+ # @example
162
+ # agentcode_includes :user, :comments, :tags, 'comments.user'
163
+ self.allowed_includes = []
164
+
165
+ # @!attribute [rw] allowed_search
166
+ # Searchable columns (full-text search across multiple fields).
167
+ #
168
+ # When +?search=term+ is used, AgentCode performs a case-insensitive LIKE
169
+ # search across all listed fields. Supports dot notation for relationships.
170
+ #
171
+ # Set via DSL: +agentcode_search :title, :content, 'user.name'+
172
+ #
173
+ # Query: +GET /api/posts?search=rails+
174
+ #
175
+ # @return [Array<String>]
176
+ # @example
177
+ # agentcode_search :title, :content, :excerpt, 'user.name'
178
+ self.allowed_search = []
179
+
180
+ # =========================================================================
181
+ # PAGINATION
182
+ # =========================================================================
183
+
184
+ # @!attribute [rw] pagination_enabled
185
+ # Whether pagination is enabled for the index endpoint.
186
+ #
187
+ # When +true+, responses include X-* pagination headers:
188
+ # +X-Current-Page+, +X-Last-Page+, +X-Per-Page+, +X-Total+.
189
+ #
190
+ # When +false+, the API returns all records. Clients can still
191
+ # request pagination via +?per_page=N+.
192
+ #
193
+ # Set via DSL: +agentcode_pagination_enabled true+
194
+ #
195
+ # @return [Boolean]
196
+ # @example
197
+ # agentcode_pagination_enabled true
198
+ # agentcode_pagination_enabled false # disable to return all records
199
+ self.pagination_enabled = false
200
+
201
+ # @!attribute [rw] agentcode_per_page_count
202
+ # Default number of records per page.
203
+ #
204
+ # Override on your model to change the default. The +?per_page+ query
205
+ # parameter overrides this value per-request (clamped 1-100).
206
+ #
207
+ # Set via DSL: +agentcode_per_page 25+
208
+ #
209
+ # @return [Integer]
210
+ # @example
211
+ # agentcode_per_page 25
212
+ # agentcode_per_page 50
213
+ self.agentcode_per_page_count = 25
214
+
215
+ # =========================================================================
216
+ # MIDDLEWARE
217
+ # =========================================================================
218
+
219
+ # @!attribute [rw] agentcode_model_middleware
220
+ # Middleware names applied to every action on this model.
221
+ #
222
+ # Set via DSL: +agentcode_middleware 'throttle:60,1', 'auth'+
223
+ #
224
+ # @return [Array<String>]
225
+ # @example
226
+ # agentcode_middleware 'throttle:60,1', 'auth'
227
+ self.agentcode_model_middleware = []
228
+
229
+ # @!attribute [rw] agentcode_middleware_actions_map
230
+ # Per-action middleware.
231
+ #
232
+ # Keys are action names: +'index'+, +'show'+, +'store'+, +'update'+,
233
+ # +'destroy'+, +'trashed'+, +'restore'+, +'force_delete'+.
234
+ #
235
+ # Set via DSL: +agentcode_middleware_actions store: ['verified']+
236
+ #
237
+ # @return [Hash{String => Array<String>}]
238
+ # @example
239
+ # agentcode_middleware_actions(
240
+ # store: ['verified'],
241
+ # update: ['verified'],
242
+ # destroy: ['admin']
243
+ # )
244
+ self.agentcode_middleware_actions_map = {}
245
+
246
+ # =========================================================================
247
+ # ROUTE EXCLUSION
248
+ # =========================================================================
249
+
250
+ # @!attribute [rw] agentcode_except_actions_list
251
+ # Actions to exclude from route registration.
252
+ #
253
+ # Available actions: +'index'+, +'show'+, +'store'+, +'update'+,
254
+ # +'destroy'+, +'trashed'+, +'restore'+, +'force_delete'+.
255
+ #
256
+ # Set via DSL: +agentcode_except_actions :destroy, :force_delete+
257
+ #
258
+ # @return [Array<String>]
259
+ # @example
260
+ # # Disable delete endpoints entirely
261
+ # agentcode_except_actions :destroy, :force_delete
262
+ # @example Read-only API
263
+ # agentcode_except_actions :store, :update, :destroy
264
+ self.agentcode_except_actions_list = []
265
+
266
+ # =========================================================================
267
+ # OWNERSHIP / MULTI-TENANCY
268
+ # =========================================================================
269
+
270
+ # @internal Auto-detected from belongs_to associations.
271
+ self.agentcode_owner_path = nil
272
+
273
+ # =========================================================================
274
+ # VALIDATION (provided by AgentCode::HasValidation)
275
+ # =========================================================================
276
+ # Format validation uses standard ActiveModel +validates+ declarations
277
+ # on your model (always with +allow_nil: true+).
278
+ #
279
+ # validates :title, length: { maximum: 255 }, allow_nil: true
280
+ # validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
281
+ #
282
+ # Field permissions (which attributes are accepted on create/update)
283
+ # are controlled by the policy. See +permitted_attributes_for_create+
284
+ # and +permitted_attributes_for_update+ on your policy class.
285
+ # =========================================================================
286
+
287
+ # Field permissions (which attributes are accepted on create/update) are
288
+ # controlled by the policy, not the model. Implement
289
+ # +permitted_attributes_for_create+ and +permitted_attributes_for_update+
290
+ # on your policy class.
291
+
292
+ # =========================================================================
293
+ # HIDDEN COLUMNS (provided by AgentCode::HidableColumns)
294
+ # =========================================================================
295
+
296
+ # @!attribute [rw] additional_hidden_columns
297
+ # Additional columns to hide from API responses (on top of base defaults).
298
+ #
299
+ # Base hidden columns (always hidden): +password+, +password_digest+,
300
+ # +remember_token+, +created_at+, +updated_at+, +deleted_at+,
301
+ # +discarded_at+, +email_verified_at+.
302
+ #
303
+ # For per-user column hiding, implement +hidden_attributes_for_show+ /
304
+ # +permitted_attributes_for_show+ on your Policy.
305
+ #
306
+ # Set via DSL: +agentcode_additional_hidden :api_token, :stripe_id+
307
+ #
308
+ # @return [Array<String>]
309
+ # @example
310
+ # agentcode_additional_hidden :api_token, :stripe_id, :internal_notes
311
+ self.additional_hidden_columns = []
312
+
313
+ # =========================================================================
314
+ # SOFT DELETES (requires Discard gem)
315
+ # =========================================================================
316
+ # Add +include Discard::Model+ to enable soft deletes.
317
+ # Requires a +discarded_at+ datetime column in your migration.
318
+ #
319
+ # When enabled, unlocks trash/restore/force-delete API endpoints.
320
+ #
321
+ # class Post < AgentCode::AgentCodeModel
322
+ # include Discard::Model
323
+ # end
324
+ # =========================================================================
325
+
326
+ # =========================================================================
327
+ # AUDIT TRAIL (requires AgentCode::HasAuditTrail concern)
328
+ # =========================================================================
329
+ # When including +AgentCode::HasAuditTrail+, every create/update/delete
330
+ # is logged to the +audit_logs+ table via ActiveRecord callbacks.
331
+ #
332
+ # Exclude sensitive fields from audit snapshots:
333
+ # agentcode_audit_exclude :password, :remember_token, :api_key
334
+ #
335
+ # Access audit logs:
336
+ # post.audit_logs.order(created_at: :desc)
337
+ #
338
+ # class Post < AgentCode::AgentCodeModel
339
+ # include AgentCode::HasAuditTrail
340
+ # agentcode_audit_exclude :password, :secret_token
341
+ # end
342
+ # =========================================================================
343
+
344
+ # =========================================================================
345
+ # MULTI-TENANCY (requires AgentCode::BelongsToOrganization concern)
346
+ # =========================================================================
347
+ # When including +AgentCode::BelongsToOrganization+:
348
+ # - +organization_id+ is auto-set from the request on create
349
+ # - A default scope filters queries by the current organization
350
+ # - +belongs_to :organization+ is set up automatically
351
+ #
352
+ # class Project < AgentCode::AgentCodeModel
353
+ # include AgentCode::BelongsToOrganization
354
+ # end
355
+ #
356
+ # For nested ownership (e.g. Task -> Project -> Organization),
357
+ # the path is auto-detected from belongs_to associations.
358
+ # =========================================================================
359
+
360
+ # =========================================================================
361
+ # UUID (requires AgentCode::HasUuid concern)
362
+ # =========================================================================
363
+ # When including +AgentCode::HasUuid+, a UUID is auto-generated on
364
+ # creation if the model has a +uuid+ column.
365
+ #
366
+ # class Post < AgentCode::AgentCodeModel
367
+ # include AgentCode::HasUuid
368
+ # end
369
+ # =========================================================================
370
+
371
+ # =========================================================================
372
+ # PERMISSIONS (requires AgentCode::HasPermissions -- User model only)
373
+ # =========================================================================
374
+ # When including +AgentCode::HasPermissions+:
375
+ # - +has_permission?(permission, organization)+ checks permissions
376
+ # - +role_slug_for_validation(organization)+ resolves the role slug
377
+ #
378
+ # Permission format: +{slug}.{action}+ e.g. +'posts.index'+
379
+ # Wildcards: +'*'+ (all) or +'posts.*'+ (all actions on posts)
380
+ #
381
+ # class User < AgentCode::AgentCodeModel
382
+ # include AgentCode::HasPermissions
383
+ # has_many :user_roles
384
+ # end
385
+ # =========================================================================
386
+ end
387
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ class AuditLog < ActiveRecord::Base
5
+ self.table_name = "audit_logs"
6
+
7
+ belongs_to :auditable, polymorphic: true
8
+ belongs_to :user, optional: true
9
+
10
+ validates :action, presence: true
11
+
12
+ # old_values and new_values are json columns (native serialization).
13
+ # No explicit `serialize` call needed — ActiveRecord handles json
14
+ # columns automatically. If using text columns instead, add
15
+ # `serialize :old_values, coder: JSON` in your app's subclass.
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ class OrganizationInvitation < ActiveRecord::Base
5
+ self.table_name = "organization_invitations"
6
+
7
+ belongs_to :organization
8
+ belongs_to :role, optional: true
9
+ belongs_to :inviter, class_name: "User", foreign_key: "invited_by", optional: true
10
+
11
+ validates :email, presence: true
12
+ validates :token, presence: true, uniqueness: true
13
+
14
+ before_create :generate_token
15
+ before_create :set_expiration
16
+
17
+ scope :pending, -> { where(status: "pending").where("expires_at > ?", Time.current) }
18
+ scope :expired, -> { where(status: "pending").where("expires_at <= ?", Time.current) }
19
+
20
+ STATUSES = %w[pending accepted expired cancelled].freeze
21
+
22
+ def expired?
23
+ status == "pending" && expires_at.present? && expires_at <= Time.current
24
+ end
25
+
26
+ def pending?
27
+ status == "pending" && !expired?
28
+ end
29
+
30
+ def accept!(user)
31
+ update!(
32
+ status: "accepted",
33
+ accepted_at: Time.current
34
+ )
35
+
36
+ # Add user to organization via pivot table
37
+ if defined?(UserRole)
38
+ UserRole.find_or_create_by!(
39
+ user_id: user.id,
40
+ organization_id: organization_id,
41
+ role_id: role_id
42
+ )
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def generate_token
49
+ self.token ||= SecureRandom.hex(32) # 64-char token
50
+ end
51
+
52
+ def set_expiration
53
+ expires_days = AgentCode.config.invitations[:expires_days] || 7
54
+ self.expires_at ||= expires_days.days.from_now
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ class InvitationPolicy
5
+ attr_reader :user, :invitation
6
+
7
+ def initialize(user, invitation)
8
+ @user = user
9
+ @invitation = invitation
10
+ end
11
+
12
+ def index?
13
+ user_belongs_to_organization?
14
+ end
15
+
16
+ def create?
17
+ user_belongs_to_organization? && role_allowed?
18
+ end
19
+
20
+ def update?
21
+ user_belongs_to_organization? && invitation.pending?
22
+ end
23
+
24
+ def destroy?
25
+ user_belongs_to_organization? && invitation.pending?
26
+ end
27
+
28
+ private
29
+
30
+ def user_belongs_to_organization?
31
+ return false unless user
32
+ return false unless invitation.respond_to?(:organization_id)
33
+
34
+ if user.respond_to?(:user_roles)
35
+ user.user_roles.exists?(organization_id: invitation.organization_id)
36
+ else
37
+ true
38
+ end
39
+ end
40
+
41
+ def role_allowed?
42
+ allowed_roles = AgentCode.config.invitations[:allowed_roles]
43
+ return true if allowed_roles.nil?
44
+
45
+ if user.respond_to?(:role_slug_for_validation)
46
+ org = invitation.respond_to?(:organization) ? invitation.organization : nil
47
+ role_slug = user.role_slug_for_validation(org)
48
+ allowed_roles.include?(role_slug)
49
+ else
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end