source_monitor 0.2.0 → 0.3.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 +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authentication-flow
|
|
3
|
+
description: Implements authentication using Rails 8 built-in generator. Use when setting up user authentication, login/logout, session management, password reset flows, or securing controllers.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails 8 Authentication
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Rails 8 includes a built-in authentication generator that creates a complete, secure authentication system without external gems.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Generate authentication
|
|
17
|
+
bin/rails generate authentication
|
|
18
|
+
|
|
19
|
+
# Run migrations
|
|
20
|
+
bin/rails db:migrate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This creates:
|
|
24
|
+
- `User` model with `has_secure_password`
|
|
25
|
+
- `Session` model for secure sessions
|
|
26
|
+
- `Current` model for request-local storage
|
|
27
|
+
- Authentication concern for controllers
|
|
28
|
+
- Session and Password controllers
|
|
29
|
+
- Login/logout views
|
|
30
|
+
|
|
31
|
+
## Generated Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
app/
|
|
35
|
+
├── models/
|
|
36
|
+
│ ├── user.rb # User with has_secure_password
|
|
37
|
+
│ ├── session.rb # Session tracking
|
|
38
|
+
│ └── current.rb # Current.user accessor
|
|
39
|
+
├── controllers/
|
|
40
|
+
│ ├── sessions_controller.rb # Login/logout
|
|
41
|
+
│ ├── passwords_controller.rb # Password reset
|
|
42
|
+
│ └── concerns/
|
|
43
|
+
│ └── authentication.rb # Auth helpers
|
|
44
|
+
└── views/
|
|
45
|
+
├── sessions/
|
|
46
|
+
│ └── new.html.erb # Login form
|
|
47
|
+
└── passwords/
|
|
48
|
+
├── new.html.erb # Forgot password
|
|
49
|
+
└── edit.html.erb # Reset password
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Core Components
|
|
53
|
+
|
|
54
|
+
### User Model
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# app/models/user.rb
|
|
58
|
+
class User < ApplicationRecord
|
|
59
|
+
has_secure_password
|
|
60
|
+
has_many :sessions, dependent: :destroy
|
|
61
|
+
|
|
62
|
+
normalizes :email_address, with: -> { _1.strip.downcase }
|
|
63
|
+
|
|
64
|
+
validates :email_address, presence: true, uniqueness: true,
|
|
65
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Session Model
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# app/models/session.rb
|
|
73
|
+
class Session < ApplicationRecord
|
|
74
|
+
belongs_to :user
|
|
75
|
+
|
|
76
|
+
before_create { self.token = SecureRandom.urlsafe_base64(32) }
|
|
77
|
+
|
|
78
|
+
def self.find_by_token(token)
|
|
79
|
+
find_by(token: token) if token.present?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Current Model
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# app/models/current.rb
|
|
88
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
89
|
+
attribute :session
|
|
90
|
+
delegate :user, to: :session, allow_nil: true
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Authentication Concern
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# app/controllers/concerns/authentication.rb
|
|
98
|
+
module Authentication
|
|
99
|
+
extend ActiveSupport::Concern
|
|
100
|
+
|
|
101
|
+
included do
|
|
102
|
+
before_action :require_authentication
|
|
103
|
+
helper_method :authenticated?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class_methods do
|
|
107
|
+
def allow_unauthenticated_access(**options)
|
|
108
|
+
skip_before_action :require_authentication, **options
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def authenticated?
|
|
115
|
+
Current.session.present?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def require_authentication
|
|
119
|
+
resume_session || request_authentication
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resume_session
|
|
123
|
+
if session_token = cookies.signed[:session_token]
|
|
124
|
+
if session = Session.find_by_token(session_token)
|
|
125
|
+
Current.session = session
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def request_authentication
|
|
131
|
+
redirect_to new_session_path
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def start_new_session_for(user)
|
|
135
|
+
session = user.sessions.create!
|
|
136
|
+
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true }
|
|
137
|
+
Current.session = session
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def terminate_session
|
|
141
|
+
Current.session&.destroy
|
|
142
|
+
cookies.delete(:session_token)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Usage Patterns
|
|
148
|
+
|
|
149
|
+
### Protecting Controllers
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
class ApplicationController < ActionController::Base
|
|
153
|
+
include Authentication
|
|
154
|
+
# All actions require authentication by default
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class HomeController < ApplicationController
|
|
158
|
+
allow_unauthenticated_access only: [:index, :about]
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Accessing Current User
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# In controllers and views
|
|
166
|
+
Current.user
|
|
167
|
+
Current.user.email_address
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Login Flow
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# app/controllers/sessions_controller.rb
|
|
174
|
+
class SessionsController < ApplicationController
|
|
175
|
+
allow_unauthenticated_access only: [:new, :create]
|
|
176
|
+
|
|
177
|
+
def new
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def create
|
|
181
|
+
if user = User.authenticate_by(email_address: params[:email_address],
|
|
182
|
+
password: params[:password])
|
|
183
|
+
start_new_session_for(user)
|
|
184
|
+
redirect_to root_path, notice: "Signed in successfully"
|
|
185
|
+
else
|
|
186
|
+
flash.now[:alert] = "Invalid email or password"
|
|
187
|
+
render :new, status: :unprocessable_entity
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def destroy
|
|
192
|
+
terminate_session
|
|
193
|
+
redirect_to root_path, notice: "Signed out"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Testing Authentication
|
|
199
|
+
|
|
200
|
+
### Test Helper
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# test/test_helper.rb
|
|
204
|
+
class ActionDispatch::IntegrationTest
|
|
205
|
+
def sign_in(user)
|
|
206
|
+
session = user.sessions.create!
|
|
207
|
+
cookies[:session_token] = session.token
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def sign_out
|
|
211
|
+
cookies.delete(:session_token)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Session Controller Tests
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# test/controllers/sessions_controller_test.rb
|
|
220
|
+
require "test_helper"
|
|
221
|
+
|
|
222
|
+
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|
223
|
+
setup do
|
|
224
|
+
@user = users(:one)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
test "GET new renders login form" do
|
|
228
|
+
get new_session_path
|
|
229
|
+
assert_response :success
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
test "POST create with valid credentials signs in user" do
|
|
233
|
+
post session_path, params: {
|
|
234
|
+
email_address: @user.email_address,
|
|
235
|
+
password: "password123"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
assert_redirected_to root_path
|
|
239
|
+
assert cookies[:session_token].present?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
test "POST create with invalid credentials shows error" do
|
|
243
|
+
post session_path, params: {
|
|
244
|
+
email_address: @user.email_address,
|
|
245
|
+
password: "wrong"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
assert_response :unprocessable_entity
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
test "DELETE destroy signs out user" do
|
|
252
|
+
sign_in @user
|
|
253
|
+
|
|
254
|
+
delete session_path
|
|
255
|
+
|
|
256
|
+
assert_redirected_to root_path
|
|
257
|
+
assert_nil cookies[:session_token]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Protected Route Tests
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# test/controllers/posts_controller_test.rb
|
|
266
|
+
require "test_helper"
|
|
267
|
+
|
|
268
|
+
class PostsControllerTest < ActionDispatch::IntegrationTest
|
|
269
|
+
setup do
|
|
270
|
+
@user = users(:one)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
test "redirects to login when not authenticated" do
|
|
274
|
+
get posts_path
|
|
275
|
+
assert_redirected_to new_session_path
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
test "shows posts when authenticated" do
|
|
279
|
+
sign_in @user
|
|
280
|
+
|
|
281
|
+
get posts_path
|
|
282
|
+
assert_response :success
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## References
|
|
288
|
+
|
|
289
|
+
- See [sessions.md](reference/sessions.md) for session management details
|
|
290
|
+
- See [current.md](reference/current.md) for Current attributes patterns
|
|
291
|
+
- See [passwordless.md](reference/passwordless.md) for magic link authentication
|
|
292
|
+
|
|
293
|
+
## Common Customizations
|
|
294
|
+
|
|
295
|
+
### Remember Me
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
def start_new_session_for(user, remember: false)
|
|
299
|
+
session = user.sessions.create!
|
|
300
|
+
cookie_options = { value: session.token, httponly: true }
|
|
301
|
+
cookie_options[:expires] = 2.weeks.from_now if remember
|
|
302
|
+
cookies.signed.permanent[:session_token] = cookie_options
|
|
303
|
+
Current.session = session
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Multiple Sessions Tracking
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
def active_sessions
|
|
311
|
+
sessions.where("created_at > ?", 30.days.ago)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def terminate_all_sessions_except(current_session)
|
|
315
|
+
sessions.where.not(id: current_session.id).destroy_all
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Rate Limiting
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
# app/controllers/sessions_controller.rb
|
|
323
|
+
rate_limit to: 10, within: 3.minutes, only: :create,
|
|
324
|
+
with: -> { redirect_to new_session_path, alert: "Too many attempts" }
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Checklist
|
|
328
|
+
|
|
329
|
+
- [ ] Authentication generator run
|
|
330
|
+
- [ ] Test helper with `sign_in`/`sign_out` methods
|
|
331
|
+
- [ ] Session controller tests written
|
|
332
|
+
- [ ] Protected route tests written
|
|
333
|
+
- [ ] Rate limiting on login
|
|
334
|
+
- [ ] `allow_unauthenticated_access` on public pages
|
|
335
|
+
- [ ] All tests GREEN
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Current Attributes Reference
|
|
2
|
+
|
|
3
|
+
## Concept
|
|
4
|
+
|
|
5
|
+
`Current` uses `ActiveSupport::CurrentAttributes` to provide request-local storage, making request-specific data available throughout the application without passing it explicitly.
|
|
6
|
+
|
|
7
|
+
## Basic Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# app/models/current.rb
|
|
11
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
12
|
+
# Attributes stored per-request
|
|
13
|
+
attribute :session
|
|
14
|
+
attribute :user_agent
|
|
15
|
+
attribute :ip_address
|
|
16
|
+
attribute :request_id
|
|
17
|
+
|
|
18
|
+
# Delegate to session for convenience
|
|
19
|
+
delegate :user, to: :session, allow_nil: true
|
|
20
|
+
|
|
21
|
+
# Resets automatically between requests
|
|
22
|
+
resets { Time.zone = nil }
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Setting Current Attributes
|
|
27
|
+
|
|
28
|
+
### In ApplicationController
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# app/controllers/application_controller.rb
|
|
32
|
+
class ApplicationController < ActionController::Base
|
|
33
|
+
before_action :set_current_attributes
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def set_current_attributes
|
|
38
|
+
Current.user_agent = request.user_agent
|
|
39
|
+
Current.ip_address = request.remote_ip
|
|
40
|
+
Current.request_id = request.request_id
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### In Authentication
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# app/controllers/concerns/authentication.rb
|
|
49
|
+
def resume_session
|
|
50
|
+
if token = cookies.signed[:session_token]
|
|
51
|
+
if session = Session.find_by_token(token)
|
|
52
|
+
Current.session = session # Sets Current.session
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Accessing Current
|
|
59
|
+
|
|
60
|
+
### In Controllers
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class PostsController < ApplicationController
|
|
64
|
+
def create
|
|
65
|
+
@post = Current.user.posts.build(post_params)
|
|
66
|
+
# ...
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def index
|
|
70
|
+
@posts = Current.user.posts
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### In Views
|
|
76
|
+
|
|
77
|
+
```erb
|
|
78
|
+
<% if Current.user %>
|
|
79
|
+
Logged in as: <%= Current.user.email_address %>
|
|
80
|
+
<%= link_to "Sign out", session_path, method: :delete %>
|
|
81
|
+
<% else %>
|
|
82
|
+
<%= link_to "Sign in", new_session_path %>
|
|
83
|
+
<% end %>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### In Models (Use Sparingly)
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class Post < ApplicationRecord
|
|
90
|
+
belongs_to :user
|
|
91
|
+
|
|
92
|
+
before_create :set_author
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def set_author
|
|
97
|
+
self.user ||= Current.user
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Warning**: Using `Current` in models couples them to the request context. This makes testing harder and breaks in background jobs. Prefer passing the user explicitly.
|
|
103
|
+
|
|
104
|
+
### In Mailers
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
class NotificationMailer < ApplicationMailer
|
|
108
|
+
def alert(user, message)
|
|
109
|
+
@user = user
|
|
110
|
+
@message = message
|
|
111
|
+
@request_id = Current.request_id # For logging correlation
|
|
112
|
+
|
|
113
|
+
mail(to: user.email)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### In Jobs (Careful!)
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class AuditLogJob < ApplicationJob
|
|
122
|
+
def perform(action:, user_id:, ip_address:, request_id:)
|
|
123
|
+
# Don't rely on Current - it's reset between requests
|
|
124
|
+
# Pass values explicitly
|
|
125
|
+
AuditLog.create!(
|
|
126
|
+
action: action,
|
|
127
|
+
user_id: user_id,
|
|
128
|
+
ip_address: ip_address,
|
|
129
|
+
request_id: request_id
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Enqueue with current values
|
|
135
|
+
AuditLogJob.perform_later(
|
|
136
|
+
action: "created_post",
|
|
137
|
+
user_id: Current.user.id,
|
|
138
|
+
ip_address: Current.ip_address,
|
|
139
|
+
request_id: Current.request_id
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Common Attributes
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
147
|
+
# Authentication
|
|
148
|
+
attribute :session
|
|
149
|
+
delegate :user, to: :session, allow_nil: true
|
|
150
|
+
|
|
151
|
+
# Request metadata
|
|
152
|
+
attribute :request_id
|
|
153
|
+
attribute :user_agent
|
|
154
|
+
attribute :ip_address
|
|
155
|
+
|
|
156
|
+
# Timezone (per-user)
|
|
157
|
+
attribute :time_zone
|
|
158
|
+
|
|
159
|
+
# Feature flags
|
|
160
|
+
attribute :feature_flags
|
|
161
|
+
|
|
162
|
+
# Request tracking
|
|
163
|
+
attribute :request_start_time
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Callbacks
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
171
|
+
attribute :session, :time_zone
|
|
172
|
+
|
|
173
|
+
# Called when session is set
|
|
174
|
+
after_reset do
|
|
175
|
+
Time.zone = nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Apply user's timezone when session is set
|
|
179
|
+
def session=(session)
|
|
180
|
+
super
|
|
181
|
+
self.time_zone = session&.user&.time_zone
|
|
182
|
+
Time.zone = time_zone if time_zone
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Testing
|
|
188
|
+
|
|
189
|
+
### Stub Current in Tests
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# test/support/current_helpers.rb
|
|
193
|
+
module CurrentHelpers
|
|
194
|
+
def with_current_user(user)
|
|
195
|
+
session = user.sessions.create!
|
|
196
|
+
Current.session = session
|
|
197
|
+
yield
|
|
198
|
+
ensure
|
|
199
|
+
Current.reset
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### In Tests
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# test/models/post_test.rb
|
|
209
|
+
require "test_helper"
|
|
210
|
+
|
|
211
|
+
class PostTest < ActiveSupport::TestCase
|
|
212
|
+
test "sets author from Current.user" do
|
|
213
|
+
user = users(:one)
|
|
214
|
+
with_current_user(user) do
|
|
215
|
+
post = Post.create!(title: "Test")
|
|
216
|
+
assert_equal user, post.user
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Controller Tests
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# test/controllers/posts_controller_test.rb
|
|
226
|
+
require "test_helper"
|
|
227
|
+
|
|
228
|
+
class PostsControllerTest < ActionDispatch::IntegrationTest
|
|
229
|
+
setup do
|
|
230
|
+
@user = users(:one)
|
|
231
|
+
sign_in_as @user # Sets Current.session
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
test "uses current user" do
|
|
235
|
+
post posts_path, params: { post: { title: "Test" } }
|
|
236
|
+
assert_equal @user, Post.last.user
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Best Practices
|
|
242
|
+
|
|
243
|
+
1. **Controllers/Views**: Safe to use `Current.user` freely
|
|
244
|
+
2. **Models**: Pass user explicitly when possible
|
|
245
|
+
3. **Jobs**: Never rely on Current - pass values explicitly
|
|
246
|
+
4. **Mailers**: Can use for metadata, but pass main data explicitly
|
|
247
|
+
5. **Services**: Accept user as parameter, don't assume Current
|
|
248
|
+
6. **Tests**: Reset Current between examples
|