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,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentCode
|
|
4
|
+
# Invitation management controller — mirrors Laravel InvitationController exactly.
|
|
5
|
+
#
|
|
6
|
+
# Endpoints:
|
|
7
|
+
# GET /api/{org}/invitations
|
|
8
|
+
# POST /api/{org}/invitations
|
|
9
|
+
# POST /api/{org}/invitations/:id/resend
|
|
10
|
+
# DELETE /api/{org}/invitations/:id
|
|
11
|
+
# POST /api/invitations/accept (public)
|
|
12
|
+
class InvitationsController < ActionController::API
|
|
13
|
+
include Pundit::Authorization
|
|
14
|
+
|
|
15
|
+
before_action :authenticate_user!, except: [:accept]
|
|
16
|
+
before_action :set_organization, except: [:accept]
|
|
17
|
+
|
|
18
|
+
# GET /api/{org}/invitations
|
|
19
|
+
def index
|
|
20
|
+
authorize OrganizationInvitation, :index?, policy_class: InvitationPolicy
|
|
21
|
+
|
|
22
|
+
status = params[:status] || "all"
|
|
23
|
+
|
|
24
|
+
query = OrganizationInvitation
|
|
25
|
+
.where(organization_id: current_organization.id)
|
|
26
|
+
.includes(:organization, :role, :inviter)
|
|
27
|
+
|
|
28
|
+
case status
|
|
29
|
+
when "pending"
|
|
30
|
+
query = query.pending
|
|
31
|
+
when "expired"
|
|
32
|
+
query = query.expired
|
|
33
|
+
when "all"
|
|
34
|
+
# no filter
|
|
35
|
+
else
|
|
36
|
+
query = query.where(status: status)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
render json: query.order(created_at: :desc)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# POST /api/{org}/invitations
|
|
43
|
+
def create
|
|
44
|
+
authorize OrganizationInvitation, :create?, policy_class: InvitationPolicy
|
|
45
|
+
|
|
46
|
+
errors = {}
|
|
47
|
+
errors[:email] = ["The email field is required."] if params[:email].blank?
|
|
48
|
+
errors[:role_id] = ["The role_id field is required."] if params[:role_id].blank?
|
|
49
|
+
|
|
50
|
+
unless errors.empty?
|
|
51
|
+
return render json: { errors: errors }, status: :unprocessable_entity
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
email = params[:email].to_s.strip
|
|
55
|
+
role_id = params[:role_id]
|
|
56
|
+
|
|
57
|
+
# Check if user already exists and is in organization
|
|
58
|
+
user_class = "User".safe_constantize
|
|
59
|
+
if user_class
|
|
60
|
+
existing_user = user_class.find_by(email: email)
|
|
61
|
+
if existing_user&.respond_to?(:organizations)
|
|
62
|
+
if existing_user.organizations.exists?(id: current_organization.id)
|
|
63
|
+
return render json: { message: "User is already a member of this organization" }, status: :unprocessable_entity
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check for existing pending invitation
|
|
69
|
+
existing_invitation = OrganizationInvitation
|
|
70
|
+
.where(email: email, organization_id: current_organization.id, status: "pending")
|
|
71
|
+
.where("expires_at IS NULL OR expires_at > ?", Time.current)
|
|
72
|
+
.first
|
|
73
|
+
|
|
74
|
+
if existing_invitation
|
|
75
|
+
return render json: { message: "A pending invitation already exists for this email" }, status: :unprocessable_entity
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Create invitation
|
|
79
|
+
invitation = OrganizationInvitation.create!(
|
|
80
|
+
organization_id: current_organization.id,
|
|
81
|
+
email: email,
|
|
82
|
+
role_id: role_id,
|
|
83
|
+
invited_by: current_user.id
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Send notification email
|
|
87
|
+
send_invitation_email(invitation)
|
|
88
|
+
|
|
89
|
+
render json: invitation.as_json(include: { organization: {}, role: {}, inviter: {} }), status: :created
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# POST /api/{org}/invitations/:id/resend
|
|
93
|
+
def resend
|
|
94
|
+
invitation = OrganizationInvitation
|
|
95
|
+
.where(id: params[:id], organization_id: current_organization.id)
|
|
96
|
+
.first!
|
|
97
|
+
|
|
98
|
+
authorize invitation, :update?, policy_class: InvitationPolicy
|
|
99
|
+
|
|
100
|
+
unless invitation.status == "pending"
|
|
101
|
+
return render json: { message: "Only pending invitations can be resent" }, status: :unprocessable_entity
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Update expiration
|
|
105
|
+
days = AgentCode.config.invitations[:expires_days] || 7
|
|
106
|
+
invitation.update!(expires_at: days.days.from_now)
|
|
107
|
+
|
|
108
|
+
# Resend notification email
|
|
109
|
+
send_invitation_email(invitation)
|
|
110
|
+
|
|
111
|
+
render json: {
|
|
112
|
+
message: "Invitation resent successfully",
|
|
113
|
+
invitation: invitation.as_json(include: { organization: {}, role: {}, inviter: {} })
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# DELETE /api/{org}/invitations/:id
|
|
118
|
+
def cancel
|
|
119
|
+
invitation = OrganizationInvitation
|
|
120
|
+
.where(id: params[:id], organization_id: current_organization.id)
|
|
121
|
+
.first!
|
|
122
|
+
|
|
123
|
+
authorize invitation, :destroy?, policy_class: InvitationPolicy
|
|
124
|
+
|
|
125
|
+
unless invitation.status == "pending"
|
|
126
|
+
return render json: { message: "Only pending invitations can be cancelled" }, status: :unprocessable_entity
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
invitation.update!(status: "cancelled")
|
|
130
|
+
|
|
131
|
+
render json: { message: "Invitation cancelled successfully" }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# POST /api/invitations/accept (public route)
|
|
135
|
+
def accept
|
|
136
|
+
if params[:token].blank?
|
|
137
|
+
return render json: { errors: { token: ["The token field is required."] } }, status: :unprocessable_entity
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
invitation = OrganizationInvitation.find_by(token: params[:token], status: "pending")
|
|
141
|
+
|
|
142
|
+
unless invitation
|
|
143
|
+
return render json: { message: "Invalid or expired invitation token" }, status: :not_found
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if invitation.expired?
|
|
147
|
+
invitation.update!(status: "expired")
|
|
148
|
+
return render json: { message: "This invitation has expired" }, status: :unprocessable_entity
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check if user is authenticated
|
|
152
|
+
user = resolve_current_user
|
|
153
|
+
|
|
154
|
+
unless user
|
|
155
|
+
return render json: {
|
|
156
|
+
invitation: invitation.as_json(include: { organization: {}, role: {} }),
|
|
157
|
+
requires_registration: true,
|
|
158
|
+
message: "Please register or login to accept this invitation"
|
|
159
|
+
}, status: :ok
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# User is authenticated, accept invitation
|
|
163
|
+
if invitation.accept!(user)
|
|
164
|
+
render json: {
|
|
165
|
+
message: "Invitation accepted successfully",
|
|
166
|
+
invitation: invitation.as_json(include: { organization: {}, role: {} }),
|
|
167
|
+
organization: invitation.organization
|
|
168
|
+
}, status: :ok
|
|
169
|
+
else
|
|
170
|
+
render json: { message: "Failed to accept invitation" }, status: :internal_server_error
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def authenticate_user!
|
|
177
|
+
unless current_user
|
|
178
|
+
render json: { message: "Unauthenticated." }, status: :unauthorized
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def current_user
|
|
183
|
+
@current_user ||= resolve_current_user
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def resolve_current_user
|
|
187
|
+
token = request.headers["Authorization"]&.sub(/\ABearer /, "")
|
|
188
|
+
return nil unless token
|
|
189
|
+
|
|
190
|
+
user_class = "User".safe_constantize
|
|
191
|
+
return nil unless user_class
|
|
192
|
+
|
|
193
|
+
if user_class.respond_to?(:find_by_api_token)
|
|
194
|
+
user_class.find_by_api_token(token)
|
|
195
|
+
elsif user_class.column_names.include?("api_token")
|
|
196
|
+
user_class.find_by(api_token: token)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def set_organization
|
|
201
|
+
# Try from middleware first, then resolve from route params
|
|
202
|
+
@organization = request.env["agentcode.organization"]
|
|
203
|
+
return if @organization
|
|
204
|
+
|
|
205
|
+
org_identifier = params[:organization]
|
|
206
|
+
return unless org_identifier.present?
|
|
207
|
+
|
|
208
|
+
org_class = "Organization".safe_constantize
|
|
209
|
+
return unless org_class
|
|
210
|
+
|
|
211
|
+
column = AgentCode.config.multi_tenant[:organization_identifier_column] || "id"
|
|
212
|
+
@organization = org_class.find_by(column => org_identifier)
|
|
213
|
+
|
|
214
|
+
if @organization
|
|
215
|
+
request.env["agentcode.organization"] = @organization
|
|
216
|
+
RequestStore.store[:agentcode_organization] = @organization if defined?(RequestStore)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def current_organization
|
|
221
|
+
@organization
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def send_invitation_email(invitation)
|
|
225
|
+
mailer_class = "AgentCode::InvitationMailer".safe_constantize
|
|
226
|
+
mailer_class&.invite(invitation)&.deliver_later
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
Rails.logger.warn("Failed to send invitation email: #{e.message}")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|