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.
- checksums.yaml +7 -0
- data/README.md +59 -0
- data/lib/agentcode/blueprint/blueprint_parser.rb +198 -0
- data/lib/agentcode/blueprint/blueprint_validator.rb +209 -0
- data/lib/agentcode/blueprint/generators/factory_generator.rb +74 -0
- data/lib/agentcode/blueprint/generators/policy_generator.rb +154 -0
- data/lib/agentcode/blueprint/generators/seeder_generator.rb +160 -0
- data/lib/agentcode/blueprint/generators/test_generator.rb +291 -0
- data/lib/agentcode/blueprint/manifest_manager.rb +81 -0
- data/lib/agentcode/commands/base_command.rb +57 -0
- data/lib/agentcode/commands/blueprint_command.rb +549 -0
- data/lib/agentcode/commands/export_postman_command.rb +328 -0
- data/lib/agentcode/commands/generate_command.rb +563 -0
- data/lib/agentcode/commands/install_command.rb +441 -0
- data/lib/agentcode/commands/invitation_link_command.rb +107 -0
- data/lib/agentcode/concerns/belongs_to_organization.rb +49 -0
- data/lib/agentcode/concerns/has_agentcode.rb +93 -0
- data/lib/agentcode/concerns/has_audit_trail.rb +125 -0
- data/lib/agentcode/concerns/has_auto_scope.rb +91 -0
- data/lib/agentcode/concerns/has_permissions.rb +117 -0
- data/lib/agentcode/concerns/has_uuid.rb +26 -0
- data/lib/agentcode/concerns/has_validation.rb +250 -0
- data/lib/agentcode/concerns/hidable_columns.rb +180 -0
- data/lib/agentcode/configuration.rb +98 -0
- data/lib/agentcode/controllers/auth_controller.rb +242 -0
- data/lib/agentcode/controllers/invitations_controller.rb +231 -0
- data/lib/agentcode/controllers/resources_controller.rb +813 -0
- data/lib/agentcode/engine.rb +65 -0
- data/lib/agentcode/mailers/invitation_mailer.rb +22 -0
- data/lib/agentcode/middleware/resolve_organization_from_route.rb +72 -0
- data/lib/agentcode/models/agentcode_model.rb +387 -0
- data/lib/agentcode/models/audit_log.rb +17 -0
- data/lib/agentcode/models/organization_invitation.rb +57 -0
- data/lib/agentcode/policies/invitation_policy.rb +54 -0
- data/lib/agentcode/policies/resource_policy.rb +197 -0
- data/lib/agentcode/query_builder.rb +278 -0
- data/lib/agentcode/railtie.rb +11 -0
- data/lib/agentcode/resource_scope.rb +59 -0
- data/lib/agentcode/routes.rb +124 -0
- data/lib/agentcode/tasks/agentcode.rake +39 -0
- data/lib/agentcode/templates/agentcode.rb +71 -0
- data/lib/agentcode/templates/agentcode_model.rb +104 -0
- data/lib/agentcode/templates/audit_trail/create_audit_logs.rb.erb +26 -0
- data/lib/agentcode/templates/generate/factory.rb.erb +43 -0
- data/lib/agentcode/templates/generate/migration.rb.erb +26 -0
- data/lib/agentcode/templates/generate/model.rb.erb +55 -0
- data/lib/agentcode/templates/generate/policy.rb.erb +52 -0
- data/lib/agentcode/templates/generate/scope.rb.erb +31 -0
- data/lib/agentcode/templates/multi_tenant/factories/organizations.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/factories/roles.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/factories/user_roles.rb.erb +10 -0
- data/lib/agentcode/templates/multi_tenant/factories/users.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_organizations.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_roles.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_user_roles.rb.erb +16 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_users.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/models/organization.rb.erb +18 -0
- data/lib/agentcode/templates/multi_tenant/models/role.rb.erb +11 -0
- data/lib/agentcode/templates/multi_tenant/models/user.rb.erb +14 -0
- data/lib/agentcode/templates/multi_tenant/models/user_role.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/policies/organization_policy.rb.erb +6 -0
- data/lib/agentcode/templates/multi_tenant/policies/role_policy.rb.erb +6 -0
- data/lib/agentcode/templates/multi_tenant/seeders/organization_seeder.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/seeders/role_seeder.rb.erb +19 -0
- data/lib/agentcode/templates/routes.rb +13 -0
- data/lib/agentcode/version.rb +5 -0
- data/lib/agentcode/views/lumina/invitation_mailer/invite.html.erb +29 -0
- data/lib/agentcode-rails.rb +3 -0
- data/lib/agentcode.rb +26 -0
- 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
|