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,331 @@
|
|
|
1
|
+
# Advanced Devise Patterns
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Multiple User Models](#multiple-user-models)
|
|
5
|
+
- [Email Confirmation](#email-confirmation)
|
|
6
|
+
- [Account Locking](#account-locking)
|
|
7
|
+
- [Password Complexity](#password-complexity)
|
|
8
|
+
- [Custom Authentication Logic](#custom-authentication-logic)
|
|
9
|
+
- [Soft Delete Users](#soft-delete-users)
|
|
10
|
+
- [Background Email Delivery](#background-email-delivery)
|
|
11
|
+
- [I18n Configuration](#i18n-configuration)
|
|
12
|
+
|
|
13
|
+
## Multiple User Models
|
|
14
|
+
|
|
15
|
+
### Setup Admin Alongside User
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
rails generate devise Admin
|
|
19
|
+
rails db:migrate
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Each model gets separate routes and helpers:
|
|
23
|
+
```ruby
|
|
24
|
+
# Routes
|
|
25
|
+
devise_for :users
|
|
26
|
+
devise_for :admins
|
|
27
|
+
|
|
28
|
+
# Controllers
|
|
29
|
+
before_action :authenticate_user! # for users
|
|
30
|
+
before_action :authenticate_admin! # for admins
|
|
31
|
+
|
|
32
|
+
# Helpers
|
|
33
|
+
user_signed_in? / current_user / user_session
|
|
34
|
+
admin_signed_in? / current_admin / admin_session
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Scoped Views
|
|
38
|
+
|
|
39
|
+
Enable in initializer:
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/devise.rb
|
|
42
|
+
config.scoped_views = true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Generate scoped views:
|
|
46
|
+
```bash
|
|
47
|
+
rails generate devise:views users
|
|
48
|
+
rails generate devise:views admins
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Views will be in `app/views/users/` and `app/views/admins/`.
|
|
52
|
+
|
|
53
|
+
## Email Confirmation
|
|
54
|
+
|
|
55
|
+
### Enable Confirmable
|
|
56
|
+
|
|
57
|
+
1. Add to model:
|
|
58
|
+
```ruby
|
|
59
|
+
devise :database_authenticatable, :registerable, :confirmable
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. Add migration columns:
|
|
63
|
+
```bash
|
|
64
|
+
rails g migration AddConfirmableToUsers confirmation_token:string confirmed_at:datetime confirmation_sent_at:datetime unconfirmed_email:string
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class AddConfirmableToUsers < ActiveRecord::Migration[7.0]
|
|
69
|
+
def change
|
|
70
|
+
add_column :users, :confirmation_token, :string
|
|
71
|
+
add_column :users, :confirmed_at, :datetime
|
|
72
|
+
add_column :users, :confirmation_sent_at, :datetime
|
|
73
|
+
add_column :users, :unconfirmed_email, :string
|
|
74
|
+
add_index :users, :confirmation_token, unique: true
|
|
75
|
+
|
|
76
|
+
# Confirm existing users
|
|
77
|
+
User.update_all(confirmed_at: Time.current)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Customize Confirmation
|
|
83
|
+
|
|
84
|
+
In `config/initializers/devise.rb`:
|
|
85
|
+
```ruby
|
|
86
|
+
config.confirm_within = 3.days # Token valid for 3 days
|
|
87
|
+
config.reconfirmable = true # Require confirmation on email change
|
|
88
|
+
config.allow_unconfirmed_access_for = 2.days # Grace period
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Skip Confirmation (Admin-created users)
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
user = User.new(email: 'test@example.com', password: 'password')
|
|
95
|
+
user.skip_confirmation!
|
|
96
|
+
user.save!
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Account Locking
|
|
100
|
+
|
|
101
|
+
### Enable Lockable
|
|
102
|
+
|
|
103
|
+
1. Add to model:
|
|
104
|
+
```ruby
|
|
105
|
+
devise :database_authenticatable, :registerable, :lockable
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
2. Add migration:
|
|
109
|
+
```bash
|
|
110
|
+
rails g migration AddLockableToUsers failed_attempts:integer unlock_token:string locked_at:datetime
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class AddLockableToUsers < ActiveRecord::Migration[7.0]
|
|
115
|
+
def change
|
|
116
|
+
add_column :users, :failed_attempts, :integer, default: 0, null: false
|
|
117
|
+
add_column :users, :unlock_token, :string
|
|
118
|
+
add_column :users, :locked_at, :datetime
|
|
119
|
+
add_index :users, :unlock_token, unique: true
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Configuration
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# config/initializers/devise.rb
|
|
128
|
+
config.lock_strategy = :failed_attempts
|
|
129
|
+
config.unlock_keys = [:email]
|
|
130
|
+
config.unlock_strategy = :both # :email, :time, or :both
|
|
131
|
+
config.maximum_attempts = 5
|
|
132
|
+
config.unlock_in = 1.hour
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Password Complexity
|
|
136
|
+
|
|
137
|
+
### Basic Validation
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# app/models/user.rb
|
|
141
|
+
validate :password_complexity
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def password_complexity
|
|
146
|
+
return if password.blank?
|
|
147
|
+
|
|
148
|
+
unless password.match?(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
|
149
|
+
errors.add :password, 'must include at least one lowercase letter, one uppercase letter, and one digit'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Using devise-security Gem
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Gemfile
|
|
158
|
+
gem 'devise-security'
|
|
159
|
+
|
|
160
|
+
# app/models/user.rb
|
|
161
|
+
devise :password_archivable, :password_expirable, :secure_validatable
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Custom Authentication Logic
|
|
165
|
+
|
|
166
|
+
### Sign in with Username OR Email
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# config/initializers/devise.rb
|
|
170
|
+
config.authentication_keys = [:login]
|
|
171
|
+
|
|
172
|
+
# app/models/user.rb
|
|
173
|
+
attr_accessor :login
|
|
174
|
+
|
|
175
|
+
def self.find_for_database_authentication(warden_conditions)
|
|
176
|
+
conditions = warden_conditions.dup
|
|
177
|
+
if (login = conditions.delete(:login))
|
|
178
|
+
where(conditions.to_h).where(
|
|
179
|
+
['lower(username) = :value OR lower(email) = :value', { value: login.downcase }]
|
|
180
|
+
).first
|
|
181
|
+
elsif conditions.key?(:username) || conditions.key?(:email)
|
|
182
|
+
where(conditions.to_h).first
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Update views to use `:login` instead of `:email`.
|
|
188
|
+
|
|
189
|
+
### Custom Account Validation
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# app/models/user.rb
|
|
193
|
+
def active_for_authentication?
|
|
194
|
+
super && approved? && !banned?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def inactive_message
|
|
198
|
+
if !approved?
|
|
199
|
+
:not_approved
|
|
200
|
+
elsif banned?
|
|
201
|
+
:banned
|
|
202
|
+
else
|
|
203
|
+
super
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Add to locale:
|
|
209
|
+
```yaml
|
|
210
|
+
# config/locales/devise.en.yml
|
|
211
|
+
en:
|
|
212
|
+
devise:
|
|
213
|
+
failure:
|
|
214
|
+
not_approved: "Your account has not been approved yet."
|
|
215
|
+
banned: "Your account has been banned."
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Soft Delete Users
|
|
219
|
+
|
|
220
|
+
Using `discard` or `paranoia` gem:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# app/models/user.rb
|
|
224
|
+
include Discard::Model
|
|
225
|
+
|
|
226
|
+
def active_for_authentication?
|
|
227
|
+
super && !discarded?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def inactive_message
|
|
231
|
+
discarded? ? :account_deleted : super
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Override destroy in registrations controller
|
|
235
|
+
# app/controllers/users/registrations_controller.rb
|
|
236
|
+
class Users::RegistrationsController < Devise::RegistrationsController
|
|
237
|
+
def destroy
|
|
238
|
+
resource.discard
|
|
239
|
+
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
|
|
240
|
+
set_flash_message! :notice, :destroyed
|
|
241
|
+
yield resource if block_given?
|
|
242
|
+
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Background Email Delivery
|
|
248
|
+
|
|
249
|
+
### Using Active Job
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# app/models/user.rb
|
|
253
|
+
def send_devise_notification(notification, *args)
|
|
254
|
+
devise_mailer.send(notification, self, *args).deliver_later
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### With Queue Priority
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
def send_devise_notification(notification, *args)
|
|
262
|
+
devise_mailer.send(notification, self, *args).deliver_later(queue: :high_priority)
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## I18n Configuration
|
|
267
|
+
|
|
268
|
+
### Flash Messages
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
# config/locales/devise.en.yml
|
|
272
|
+
en:
|
|
273
|
+
devise:
|
|
274
|
+
sessions:
|
|
275
|
+
signed_in: "Welcome back!"
|
|
276
|
+
signed_out: "Goodbye!"
|
|
277
|
+
already_signed_out: "You're already signed out."
|
|
278
|
+
registrations:
|
|
279
|
+
signed_up: "Welcome! You have signed up successfully."
|
|
280
|
+
signed_up_but_unconfirmed: "Please check your email to confirm your account."
|
|
281
|
+
updated: "Your account has been updated successfully."
|
|
282
|
+
destroyed: "Your account has been deleted. We're sorry to see you go!"
|
|
283
|
+
passwords:
|
|
284
|
+
send_instructions: "You will receive an email with instructions shortly."
|
|
285
|
+
updated: "Your password has been changed successfully."
|
|
286
|
+
confirmations:
|
|
287
|
+
confirmed: "Your email has been confirmed."
|
|
288
|
+
send_instructions: "Confirmation instructions sent."
|
|
289
|
+
unlocks:
|
|
290
|
+
send_instructions: "Unlock instructions sent."
|
|
291
|
+
unlocked: "Your account has been unlocked."
|
|
292
|
+
mailer:
|
|
293
|
+
confirmation_instructions:
|
|
294
|
+
subject: "Confirm your email"
|
|
295
|
+
reset_password_instructions:
|
|
296
|
+
subject: "Reset your password"
|
|
297
|
+
unlock_instructions:
|
|
298
|
+
subject: "Unlock your account"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Per-Resource Messages
|
|
302
|
+
|
|
303
|
+
```yaml
|
|
304
|
+
en:
|
|
305
|
+
devise:
|
|
306
|
+
sessions:
|
|
307
|
+
user:
|
|
308
|
+
signed_in: "Welcome, user!"
|
|
309
|
+
admin:
|
|
310
|
+
signed_in: "Welcome back, administrator!"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Customizing Mailer
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
# app/mailers/custom_devise_mailer.rb
|
|
317
|
+
class CustomDeviseMailer < Devise::Mailer
|
|
318
|
+
helper :application
|
|
319
|
+
include Devise::Controllers::UrlHelpers
|
|
320
|
+
default template_path: 'devise/mailer'
|
|
321
|
+
layout 'mailer'
|
|
322
|
+
|
|
323
|
+
def confirmation_instructions(record, token, opts = {})
|
|
324
|
+
opts[:subject] = "Welcome to MyApp - Please confirm your email"
|
|
325
|
+
super
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# config/initializers/devise.rb
|
|
330
|
+
config.mailer = 'CustomDeviseMailer'
|
|
331
|
+
```
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# API Authentication with Devise
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Rails API Mode Setup](#rails-api-mode-setup)
|
|
5
|
+
- [HTTP Basic Authentication](#http-basic-authentication)
|
|
6
|
+
- [Token Authentication](#token-authentication)
|
|
7
|
+
- [JWT Authentication](#jwt-authentication)
|
|
8
|
+
- [Testing API Authentication](#testing-api-authentication)
|
|
9
|
+
|
|
10
|
+
## Rails API Mode Setup
|
|
11
|
+
|
|
12
|
+
For `rails new myapp --api` applications:
|
|
13
|
+
|
|
14
|
+
### Enable Required Middleware
|
|
15
|
+
|
|
16
|
+
In `config/application.rb`:
|
|
17
|
+
```ruby
|
|
18
|
+
config.middleware.use ActionDispatch::Cookies
|
|
19
|
+
config.middleware.use ActionDispatch::Session::CookieStore
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or for testing only in `config/environments/test.rb`:
|
|
23
|
+
```ruby
|
|
24
|
+
Rails.application.config.middleware.insert_before Warden::Manager, ActionDispatch::Cookies
|
|
25
|
+
Rails.application.config.middleware.insert_before Warden::Manager, ActionDispatch::Session::CookieStore
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Disable Views
|
|
29
|
+
|
|
30
|
+
In `config/initializers/devise.rb`:
|
|
31
|
+
```ruby
|
|
32
|
+
config.navigational_formats = []
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## HTTP Basic Authentication
|
|
36
|
+
|
|
37
|
+
Enable in initializer:
|
|
38
|
+
```ruby
|
|
39
|
+
# config/initializers/devise.rb
|
|
40
|
+
config.http_authenticatable = [:database]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
```bash
|
|
45
|
+
curl -u user@example.com:password http://localhost:3000/api/resource
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Token Authentication
|
|
49
|
+
|
|
50
|
+
### Simple Token Setup
|
|
51
|
+
|
|
52
|
+
1. Add token column:
|
|
53
|
+
```bash
|
|
54
|
+
rails g migration AddAuthenticationTokenToUsers authentication_token:string:index
|
|
55
|
+
rails db:migrate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. Configure User model:
|
|
59
|
+
```ruby
|
|
60
|
+
class User < ApplicationRecord
|
|
61
|
+
devise :database_authenticatable, :registerable,
|
|
62
|
+
:recoverable, :rememberable, :validatable
|
|
63
|
+
|
|
64
|
+
before_save :ensure_authentication_token
|
|
65
|
+
|
|
66
|
+
def ensure_authentication_token
|
|
67
|
+
self.authentication_token ||= generate_authentication_token
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def regenerate_authentication_token!
|
|
71
|
+
self.authentication_token = generate_authentication_token
|
|
72
|
+
save!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def generate_authentication_token
|
|
78
|
+
loop do
|
|
79
|
+
token = Devise.friendly_token
|
|
80
|
+
break token unless User.exists?(authentication_token: token)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
3. Create token authentication concern:
|
|
87
|
+
```ruby
|
|
88
|
+
# app/controllers/concerns/api_authenticatable.rb
|
|
89
|
+
module ApiAuthenticatable
|
|
90
|
+
extend ActiveSupport::Concern
|
|
91
|
+
|
|
92
|
+
included do
|
|
93
|
+
before_action :authenticate_with_token!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def authenticate_with_token!
|
|
99
|
+
authenticate_or_request_with_http_token do |token, _options|
|
|
100
|
+
@current_user = User.find_by(authentication_token: token)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def current_user
|
|
105
|
+
@current_user
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
4. Use in API controller:
|
|
111
|
+
```ruby
|
|
112
|
+
class Api::V1::BaseController < ActionController::API
|
|
113
|
+
include ApiAuthenticatable
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Usage:
|
|
118
|
+
```bash
|
|
119
|
+
curl -H "Authorization: Token token=YOUR_TOKEN" http://localhost:3000/api/v1/resource
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## JWT Authentication
|
|
123
|
+
|
|
124
|
+
Use `devise-jwt` gem for JWT-based authentication:
|
|
125
|
+
|
|
126
|
+
1. Add gems:
|
|
127
|
+
```ruby
|
|
128
|
+
gem 'devise-jwt'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
2. Configure secret:
|
|
132
|
+
```ruby
|
|
133
|
+
# config/initializers/devise.rb
|
|
134
|
+
Devise.setup do |config|
|
|
135
|
+
config.jwt do |jwt|
|
|
136
|
+
jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
|
|
137
|
+
jwt.dispatch_requests = [
|
|
138
|
+
['POST', %r{^/login$}]
|
|
139
|
+
]
|
|
140
|
+
jwt.revocation_requests = [
|
|
141
|
+
['DELETE', %r{^/logout$}]
|
|
142
|
+
]
|
|
143
|
+
jwt.expiration_time = 1.day.to_i
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
3. Set up revocation strategy:
|
|
149
|
+
```ruby
|
|
150
|
+
# app/models/user.rb
|
|
151
|
+
class User < ApplicationRecord
|
|
152
|
+
include Devise::JWT::RevocationStrategies::JTIMatcher
|
|
153
|
+
|
|
154
|
+
devise :database_authenticatable, :registerable,
|
|
155
|
+
:jwt_authenticatable, jwt_revocation_strategy: self
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
4. Add JTI column:
|
|
160
|
+
```bash
|
|
161
|
+
rails g migration AddJtiToUsers jti:string:index:unique
|
|
162
|
+
rails db:migrate
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Update migration to add NOT NULL and default:
|
|
166
|
+
```ruby
|
|
167
|
+
add_column :users, :jti, :string, null: false, default: ""
|
|
168
|
+
add_index :users, :jti, unique: true
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
5. Create sessions controller:
|
|
172
|
+
```ruby
|
|
173
|
+
# app/controllers/api/v1/sessions_controller.rb
|
|
174
|
+
class Api::V1::SessionsController < Devise::SessionsController
|
|
175
|
+
respond_to :json
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def respond_with(resource, _opts = {})
|
|
180
|
+
render json: { user: resource, token: request.env['warden-jwt_auth.token'] }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def respond_to_on_destroy
|
|
184
|
+
head :no_content
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
6. Routes:
|
|
190
|
+
```ruby
|
|
191
|
+
devise_for :users, path: '', path_names: {
|
|
192
|
+
sign_in: 'login',
|
|
193
|
+
sign_out: 'logout',
|
|
194
|
+
registration: 'signup'
|
|
195
|
+
}, controllers: {
|
|
196
|
+
sessions: 'api/v1/sessions',
|
|
197
|
+
registrations: 'api/v1/registrations'
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Usage:
|
|
202
|
+
```bash
|
|
203
|
+
# Login
|
|
204
|
+
curl -X POST http://localhost:3000/login \
|
|
205
|
+
-H "Content-Type: application/json" \
|
|
206
|
+
-d '{"user":{"email":"user@example.com","password":"password"}}'
|
|
207
|
+
|
|
208
|
+
# Use token
|
|
209
|
+
curl http://localhost:3000/api/v1/resource \
|
|
210
|
+
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## API Response Format
|
|
214
|
+
|
|
215
|
+
Create consistent JSON responses:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# app/controllers/api/v1/registrations_controller.rb
|
|
219
|
+
class Api::V1::RegistrationsController < Devise::RegistrationsController
|
|
220
|
+
respond_to :json
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def respond_with(resource, _opts = {})
|
|
225
|
+
if resource.persisted?
|
|
226
|
+
render json: {
|
|
227
|
+
status: { code: 200, message: 'Signed up successfully.' },
|
|
228
|
+
data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
render json: {
|
|
232
|
+
status: { code: 422, message: "User couldn't be created." },
|
|
233
|
+
errors: resource.errors.full_messages
|
|
234
|
+
}, status: :unprocessable_entity
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Testing API Authentication
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# spec/support/api_helpers.rb
|
|
244
|
+
module ApiHelpers
|
|
245
|
+
def auth_headers(user)
|
|
246
|
+
token = Warden::JWTAuth::UserEncoder.new.call(user, :user, nil).first
|
|
247
|
+
{ 'Authorization' => "Bearer #{token}" }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
RSpec.configure do |config|
|
|
252
|
+
config.include ApiHelpers, type: :request
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Usage in tests:
|
|
257
|
+
```ruby
|
|
258
|
+
describe 'GET /api/v1/profile' do
|
|
259
|
+
let(:user) { create(:user) }
|
|
260
|
+
|
|
261
|
+
it 'returns user profile' do
|
|
262
|
+
get '/api/v1/profile', headers: auth_headers(user)
|
|
263
|
+
expect(response).to have_http_status(:ok)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|