rails_claude_skills 0.1.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/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
- data/.github/ISSUE_TEMPLATE/question.yml +90 -0
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/release.yml +66 -0
- data/.rubocop.yml +52 -0
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +332 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +580 -0
- data/LICENSE.txt +21 -0
- data/README.md +544 -0
- data/Rakefile +8 -0
- data/lib/generators/claude/agent/agent_generator.rb +71 -0
- data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
- data/lib/generators/claude/command/command_generator.rb +50 -0
- data/lib/generators/claude/command/templates/command.md.tt +28 -0
- data/lib/generators/claude/commands_library/create-pr.md +27 -0
- data/lib/generators/claude/commands_library/dbchange.md +19 -0
- data/lib/generators/claude/commands_library/quality.md +20 -0
- data/lib/generators/claude/commands_library/stimulus.md +19 -0
- data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
- data/lib/generators/claude/install/install_generator.rb +211 -0
- data/lib/generators/claude/install/templates/README.md.tt +59 -0
- data/lib/generators/claude/install/templates/USAGE +28 -0
- data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
- data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
- data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
- data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
- data/lib/generators/claude/rule/rule_generator.rb +175 -0
- data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
- data/lib/generators/claude/rules_library/code-style.md +37 -0
- data/lib/generators/claude/rules_library/database.md +47 -0
- data/lib/generators/claude/rules_library/hotwire.md +56 -0
- data/lib/generators/claude/rules_library/security.md +54 -0
- data/lib/generators/claude/rules_library/testing.md +47 -0
- data/lib/generators/claude/skill/skill_generator.rb +196 -0
- data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
- data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
- data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
- data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
- data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
- data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
- data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
- data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
- data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
- data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
- data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
- data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
- data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
- data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
- data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
- data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
- data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
- data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
- data/lib/generators/claude/views/views_generator.rb +113 -0
- data/lib/rails_claude_skills/railtie.rb +16 -0
- data/lib/rails_claude_skills/version.rb +5 -0
- data/lib/rails_claude_skills.rb +27 -0
- data/sig/rails_claude_skills.rbs +4 -0
- metadata +199 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# API Authorization with CanCanCan
|
|
2
|
+
|
|
3
|
+
Implementing authorization for JSON APIs with CanCanCan, including token-based auth and proper error handling.
|
|
4
|
+
|
|
5
|
+
## Basic API Setup
|
|
6
|
+
|
|
7
|
+
### API Base Controller
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# app/controllers/api/v1/base_controller.rb
|
|
11
|
+
module Api
|
|
12
|
+
module V1
|
|
13
|
+
class BaseController < ActionController::API
|
|
14
|
+
before_action :authenticate_api_user!
|
|
15
|
+
|
|
16
|
+
rescue_from CanCan::AccessDenied do |exception|
|
|
17
|
+
render json: {
|
|
18
|
+
error: 'Forbidden',
|
|
19
|
+
message: exception.message
|
|
20
|
+
}, status: :forbidden
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def authenticate_api_user!
|
|
26
|
+
token = request.headers['Authorization']&.split(' ')&.last
|
|
27
|
+
@current_user = User.find_by(api_token: token)
|
|
28
|
+
|
|
29
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current_user
|
|
33
|
+
@current_user
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def current_ability
|
|
37
|
+
@current_ability ||= Ability.new(current_user)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### API Resource Controller
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# app/controllers/api/v1/posts_controller.rb
|
|
48
|
+
module Api
|
|
49
|
+
module V1
|
|
50
|
+
class PostsController < BaseController
|
|
51
|
+
load_and_authorize_resource
|
|
52
|
+
|
|
53
|
+
def index
|
|
54
|
+
render json: @posts
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def show
|
|
58
|
+
render json: @post
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create
|
|
62
|
+
if @post.save
|
|
63
|
+
render json: @post, status: :created
|
|
64
|
+
else
|
|
65
|
+
render json: { errors: @post.errors }, status: :unprocessable_entity
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def update
|
|
70
|
+
if @post.update(post_params)
|
|
71
|
+
render json: @post
|
|
72
|
+
else
|
|
73
|
+
render json: { errors: @post.errors }, status: :unprocessable_entity
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def destroy
|
|
78
|
+
@post.destroy
|
|
79
|
+
head :no_content
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def post_params
|
|
85
|
+
params.require(:post).permit(:title, :body, :published)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## JWT Authentication
|
|
93
|
+
|
|
94
|
+
### Setup with devise-jwt
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Gemfile
|
|
98
|
+
gem 'devise'
|
|
99
|
+
gem 'devise-jwt'
|
|
100
|
+
|
|
101
|
+
# app/models/user.rb
|
|
102
|
+
class User < ApplicationRecord
|
|
103
|
+
devise :database_authenticatable,
|
|
104
|
+
:jwt_authenticatable,
|
|
105
|
+
jwt_revocation_strategy: JwtDenylist
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# app/models/jwt_denylist.rb
|
|
109
|
+
class JwtDenylist < ApplicationRecord
|
|
110
|
+
include Devise::JWT::RevocationStrategies::Denylist
|
|
111
|
+
|
|
112
|
+
self.table_name = 'jwt_denylist'
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### API Controller with JWT
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# app/controllers/api/v1/base_controller.rb
|
|
120
|
+
module Api
|
|
121
|
+
module V1
|
|
122
|
+
class BaseController < ActionController::API
|
|
123
|
+
before_action :authenticate_user!
|
|
124
|
+
|
|
125
|
+
rescue_from CanCan::AccessDenied do |exception|
|
|
126
|
+
render json: {
|
|
127
|
+
error: 'Forbidden',
|
|
128
|
+
message: exception.message,
|
|
129
|
+
action: exception.action,
|
|
130
|
+
subject: exception.subject.class.name
|
|
131
|
+
}, status: :forbidden
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def current_ability
|
|
135
|
+
@current_ability ||= Ability.new(current_user)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Token-Based Authorization
|
|
143
|
+
|
|
144
|
+
### Simple Token Authentication
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# app/models/user.rb
|
|
148
|
+
class User < ApplicationRecord
|
|
149
|
+
has_secure_token :api_token
|
|
150
|
+
|
|
151
|
+
def regenerate_api_token
|
|
152
|
+
regenerate_api_token!
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Migration
|
|
157
|
+
class AddApiTokenToUsers < ActiveRecord::Migration[7.0]
|
|
158
|
+
def change
|
|
159
|
+
add_column :users, :api_token, :string
|
|
160
|
+
add_index :users, :api_token, unique: true
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Token Authentication Controller
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# app/controllers/api/v1/authentication_controller.rb
|
|
169
|
+
module Api
|
|
170
|
+
module V1
|
|
171
|
+
class AuthenticationController < ActionController::API
|
|
172
|
+
def create
|
|
173
|
+
user = User.find_by(email: params[:email])
|
|
174
|
+
|
|
175
|
+
if user&.authenticate(params[:password])
|
|
176
|
+
render json: {
|
|
177
|
+
token: user.api_token,
|
|
178
|
+
user: {
|
|
179
|
+
id: user.id,
|
|
180
|
+
email: user.email,
|
|
181
|
+
role: user.role
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
render json: { error: 'Invalid credentials' }, status: :unauthorized
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def destroy
|
|
190
|
+
current_user.regenerate_api_token
|
|
191
|
+
head :no_content
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Granular API Permissions
|
|
199
|
+
|
|
200
|
+
### API-Specific Abilities
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# app/models/ability.rb
|
|
204
|
+
class Ability
|
|
205
|
+
include CanCan::Ability
|
|
206
|
+
|
|
207
|
+
def initialize(user, options = {})
|
|
208
|
+
return unless user.present?
|
|
209
|
+
|
|
210
|
+
# Different permissions for API vs web
|
|
211
|
+
if options[:api]
|
|
212
|
+
define_api_abilities(user)
|
|
213
|
+
else
|
|
214
|
+
define_web_abilities(user)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def define_api_abilities(user)
|
|
221
|
+
# API users have more restricted access
|
|
222
|
+
can :read, Post, published: true
|
|
223
|
+
can :read, Post, user_id: user.id
|
|
224
|
+
|
|
225
|
+
if user.api_access_level == 'full'
|
|
226
|
+
can :create, Post
|
|
227
|
+
can :update, Post, user_id: user.id
|
|
228
|
+
can :destroy, Post, user_id: user.id
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Admins have full API access
|
|
232
|
+
can :manage, :all if user.admin?
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def define_web_abilities(user)
|
|
236
|
+
# Web users have standard access
|
|
237
|
+
can :read, Post
|
|
238
|
+
can :create, Post
|
|
239
|
+
can :update, Post, user_id: user.id
|
|
240
|
+
can :destroy, Post, user_id: user.id
|
|
241
|
+
|
|
242
|
+
can :manage, :all if user.admin?
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# app/controllers/api/v1/base_controller.rb
|
|
247
|
+
def current_ability
|
|
248
|
+
@current_ability ||= Ability.new(current_user, api: true)
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Scoped API Keys
|
|
253
|
+
|
|
254
|
+
### Per-Resource API Keys
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
# app/models/api_key.rb
|
|
258
|
+
class ApiKey < ApplicationRecord
|
|
259
|
+
belongs_to :user
|
|
260
|
+
|
|
261
|
+
enum scope: {
|
|
262
|
+
read_only: 0,
|
|
263
|
+
read_write: 1,
|
|
264
|
+
admin: 2
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
has_secure_token :token
|
|
268
|
+
|
|
269
|
+
def can_write?
|
|
270
|
+
read_write? || admin?
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# app/models/ability.rb
|
|
275
|
+
class Ability
|
|
276
|
+
include CanCan::Ability
|
|
277
|
+
|
|
278
|
+
def initialize(user, api_key: nil)
|
|
279
|
+
return unless user.present?
|
|
280
|
+
|
|
281
|
+
if api_key
|
|
282
|
+
define_api_key_abilities(user, api_key)
|
|
283
|
+
else
|
|
284
|
+
define_standard_abilities(user)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def define_api_key_abilities(user, api_key)
|
|
291
|
+
case api_key.scope
|
|
292
|
+
when 'read_only'
|
|
293
|
+
can :read, Post, user_id: user.id
|
|
294
|
+
when 'read_write'
|
|
295
|
+
can :read, Post
|
|
296
|
+
can [:create, :update], Post, user_id: user.id
|
|
297
|
+
when 'admin'
|
|
298
|
+
can :manage, :all
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# app/controllers/api/v1/base_controller.rb
|
|
304
|
+
def authenticate_api_user!
|
|
305
|
+
token = request.headers['Authorization']&.split(' ')&.last
|
|
306
|
+
@api_key = ApiKey.find_by(token: token)
|
|
307
|
+
|
|
308
|
+
if @api_key
|
|
309
|
+
@current_user = @api_key.user
|
|
310
|
+
else
|
|
311
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def current_ability
|
|
316
|
+
@current_ability ||= Ability.new(current_user, api_key: @api_key)
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Rate Limiting with Authorization
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# app/controllers/api/v1/base_controller.rb
|
|
324
|
+
module Api
|
|
325
|
+
module V1
|
|
326
|
+
class BaseController < ActionController::API
|
|
327
|
+
before_action :authenticate_api_user!
|
|
328
|
+
before_action :check_rate_limit
|
|
329
|
+
|
|
330
|
+
private
|
|
331
|
+
|
|
332
|
+
def check_rate_limit
|
|
333
|
+
if current_user.admin?
|
|
334
|
+
# Admins have higher rate limits
|
|
335
|
+
rate_limit = 1000
|
|
336
|
+
elsif can? :manage, Post
|
|
337
|
+
# Power users
|
|
338
|
+
rate_limit = 500
|
|
339
|
+
else
|
|
340
|
+
# Regular users
|
|
341
|
+
rate_limit = 100
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Implement rate limiting logic
|
|
345
|
+
# (using Redis, rack-attack, etc.)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Error Handling and Responses
|
|
353
|
+
|
|
354
|
+
### Detailed Error Messages
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# app/controllers/api/v1/base_controller.rb
|
|
358
|
+
rescue_from CanCan::AccessDenied do |exception|
|
|
359
|
+
render json: {
|
|
360
|
+
error: {
|
|
361
|
+
type: 'Forbidden',
|
|
362
|
+
message: exception.message,
|
|
363
|
+
details: {
|
|
364
|
+
action: exception.action,
|
|
365
|
+
subject: exception.subject.class.name,
|
|
366
|
+
subject_id: exception.subject.try(:id)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}, status: :forbidden
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Example response:
|
|
373
|
+
# {
|
|
374
|
+
# "error": {
|
|
375
|
+
# "type": "Forbidden",
|
|
376
|
+
# "message": "You are not authorized to update this Post",
|
|
377
|
+
# "details": {
|
|
378
|
+
# "action": "update",
|
|
379
|
+
# "subject": "Post",
|
|
380
|
+
# "subject_id": 123
|
|
381
|
+
# }
|
|
382
|
+
# }
|
|
383
|
+
# }
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Custom Authorization Messages
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
# app/controllers/api/v1/posts_controller.rb
|
|
390
|
+
def update
|
|
391
|
+
authorize! :update, @post, message: "You can only edit your own posts"
|
|
392
|
+
|
|
393
|
+
if @post.update(post_params)
|
|
394
|
+
render json: @post
|
|
395
|
+
else
|
|
396
|
+
render json: { errors: @post.errors }, status: :unprocessable_entity
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Testing API Authorization
|
|
402
|
+
|
|
403
|
+
### Request Specs
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# spec/requests/api/v1/posts_spec.rb
|
|
407
|
+
require 'rails_helper'
|
|
408
|
+
|
|
409
|
+
RSpec.describe 'Api::V1::Posts', type: :request do
|
|
410
|
+
let(:user) { create(:user) }
|
|
411
|
+
let(:other_user) { create(:user) }
|
|
412
|
+
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
|
|
413
|
+
|
|
414
|
+
describe 'GET /api/v1/posts' do
|
|
415
|
+
let!(:posts) { create_list(:post, 3, user: user) }
|
|
416
|
+
|
|
417
|
+
it 'returns authorized posts' do
|
|
418
|
+
get '/api/v1/posts', headers: headers
|
|
419
|
+
expect(response).to have_http_status(:ok)
|
|
420
|
+
expect(JSON.parse(response.body).size).to eq(3)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it 'requires authentication' do
|
|
424
|
+
get '/api/v1/posts'
|
|
425
|
+
expect(response).to have_http_status(:unauthorized)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
describe 'PUT /api/v1/posts/:id' do
|
|
430
|
+
let(:post) { create(:post, user: user) }
|
|
431
|
+
let(:other_post) { create(:post, user: other_user) }
|
|
432
|
+
|
|
433
|
+
it 'allows updating own post' do
|
|
434
|
+
put "/api/v1/posts/#{post.id}",
|
|
435
|
+
params: { post: { title: 'New Title' } },
|
|
436
|
+
headers: headers
|
|
437
|
+
|
|
438
|
+
expect(response).to have_http_status(:ok)
|
|
439
|
+
expect(JSON.parse(response.body)['title']).to eq('New Title')
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it 'denies updating other user post' do
|
|
443
|
+
put "/api/v1/posts/#{other_post.id}",
|
|
444
|
+
params: { post: { title: 'New Title' } },
|
|
445
|
+
headers: headers
|
|
446
|
+
|
|
447
|
+
expect(response).to have_http_status(:forbidden)
|
|
448
|
+
expect(JSON.parse(response.body)).to have_key('error')
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Testing Different API Key Scopes
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
# spec/requests/api/v1/posts_with_api_keys_spec.rb
|
|
458
|
+
RSpec.describe 'Api::V1::Posts with API keys', type: :request do
|
|
459
|
+
let(:user) { create(:user) }
|
|
460
|
+
let(:read_only_key) { create(:api_key, user: user, scope: :read_only) }
|
|
461
|
+
let(:read_write_key) { create(:api_key, user: user, scope: :read_write) }
|
|
462
|
+
|
|
463
|
+
describe 'with read-only key' do
|
|
464
|
+
let(:headers) { { 'Authorization' => "Bearer #{read_only_key.token}" } }
|
|
465
|
+
|
|
466
|
+
it 'allows reading posts' do
|
|
467
|
+
get '/api/v1/posts', headers: headers
|
|
468
|
+
expect(response).to have_http_status(:ok)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
it 'denies creating posts' do
|
|
472
|
+
post '/api/v1/posts',
|
|
473
|
+
params: { post: { title: 'Test' } },
|
|
474
|
+
headers: headers
|
|
475
|
+
|
|
476
|
+
expect(response).to have_http_status(:forbidden)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
describe 'with read-write key' do
|
|
481
|
+
let(:headers) { { 'Authorization' => "Bearer #{read_write_key.token}" } }
|
|
482
|
+
|
|
483
|
+
it 'allows creating posts' do
|
|
484
|
+
post '/api/v1/posts',
|
|
485
|
+
params: { post: { title: 'Test', body: 'Content' } },
|
|
486
|
+
headers: headers
|
|
487
|
+
|
|
488
|
+
expect(response).to have_http_status(:created)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## GraphQL Integration
|
|
495
|
+
|
|
496
|
+
```ruby
|
|
497
|
+
# app/graphql/types/query_type.rb
|
|
498
|
+
module Types
|
|
499
|
+
class QueryType < Types::BaseObject
|
|
500
|
+
field :posts, [Types::PostType], null: false
|
|
501
|
+
|
|
502
|
+
def posts
|
|
503
|
+
Post.accessible_by(context[:current_ability])
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# app/graphql/mutations/create_post.rb
|
|
509
|
+
module Mutations
|
|
510
|
+
class CreatePost < BaseMutation
|
|
511
|
+
argument :title, String, required: true
|
|
512
|
+
argument :body, String, required: true
|
|
513
|
+
|
|
514
|
+
field :post, Types::PostType, null: true
|
|
515
|
+
field :errors, [String], null: false
|
|
516
|
+
|
|
517
|
+
def resolve(title:, body:)
|
|
518
|
+
post = Post.new(title: title, body: body, user: context[:current_user])
|
|
519
|
+
|
|
520
|
+
context[:current_ability].authorize! :create, post
|
|
521
|
+
|
|
522
|
+
if post.save
|
|
523
|
+
{ post: post, errors: [] }
|
|
524
|
+
else
|
|
525
|
+
{ post: nil, errors: post.errors.full_messages }
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Best Practices for API Authorization
|
|
533
|
+
|
|
534
|
+
1. **Always authenticate API requests**: Never skip authentication for API endpoints
|
|
535
|
+
2. **Use HTTPS**: Always use SSL/TLS for API requests with sensitive tokens
|
|
536
|
+
3. **Implement rate limiting**: Protect against abuse based on user abilities
|
|
537
|
+
4. **Return proper HTTP status codes**: 401 for unauthenticated, 403 for unauthorized
|
|
538
|
+
5. **Provide clear error messages**: Help API consumers understand authorization failures
|
|
539
|
+
6. **Version your API**: Include authorization changes in API versioning
|
|
540
|
+
7. **Log authorization failures**: Track unauthorized access attempts
|
|
541
|
+
8. **Use scoped tokens**: Limit token permissions to minimum required access
|
|
542
|
+
9. **Implement token expiration**: Refresh tokens periodically
|
|
543
|
+
10. **Test authorization thoroughly**: Cover all permission scenarios in specs
|