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,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