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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,253 @@
1
+ # Passwordless Authentication (Magic Links)
2
+
3
+ Alternative to password-based auth. Based on 37signals patterns.
4
+
5
+ ## Philosophy
6
+
7
+ Auth is simple. A basic system is ~150 lines of code total. You get full control, no bloat, and easier maintenance.
8
+
9
+ ## Core Models
10
+
11
+ ### Identity Model
12
+
13
+ ```ruby
14
+ # app/models/identity.rb
15
+ class Identity < ApplicationRecord
16
+ has_secure_password validations: false
17
+
18
+ has_many :sessions, dependent: :destroy
19
+ has_many :magic_links, dependent: :destroy
20
+ has_one :user, dependent: :destroy
21
+
22
+ validates :email_address, presence: true, uniqueness: { case_sensitive: false }
23
+ validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }
24
+
25
+ normalizes :email_address, with: -> { _1.strip.downcase }
26
+
27
+ def send_magic_link(purpose: "sign_in")
28
+ magic_link = magic_links.create!(purpose: purpose)
29
+ MagicLinkMailer.sign_in_instructions(magic_link).deliver_later
30
+ magic_link
31
+ end
32
+ end
33
+ ```
34
+
35
+ ### Magic Link Model
36
+
37
+ ```ruby
38
+ # app/models/magic_link.rb
39
+ class MagicLink < ApplicationRecord
40
+ CODE_LENGTH = 6
41
+
42
+ belongs_to :identity
43
+
44
+ before_create :set_code
45
+ before_create :set_expiration
46
+
47
+ scope :unused, -> { where(used_at: nil) }
48
+ scope :active, -> { unused.where("expires_at > ?", Time.current) }
49
+
50
+ def self.authenticate(code)
51
+ active.find_by(code: code.upcase)&.tap do |magic_link|
52
+ magic_link.update!(used_at: Time.current)
53
+ end
54
+ end
55
+
56
+ def expired?
57
+ expires_at < Time.current
58
+ end
59
+
60
+ def used?
61
+ used_at.present?
62
+ end
63
+
64
+ def valid_for_use?
65
+ !expired? && !used?
66
+ end
67
+
68
+ private
69
+
70
+ def set_code
71
+ self.code = SecureRandom.alphanumeric(CODE_LENGTH).upcase
72
+ end
73
+
74
+ def set_expiration
75
+ self.expires_at = 15.minutes.from_now
76
+ end
77
+ end
78
+ ```
79
+
80
+ ### Session Model
81
+
82
+ ```ruby
83
+ # app/models/session.rb
84
+ class Session < ApplicationRecord
85
+ belongs_to :identity
86
+
87
+ has_secure_token length: 36
88
+
89
+ def active?
90
+ created_at > 30.days.ago
91
+ end
92
+ end
93
+ ```
94
+
95
+ ## Controllers
96
+
97
+ ### Sessions Controller
98
+
99
+ ```ruby
100
+ class SessionsController < ApplicationController
101
+ allow_unauthenticated_access only: [:new, :create]
102
+
103
+ def new
104
+ end
105
+
106
+ def create
107
+ if identity = Identity.find_by(email_address: params[:email_address])
108
+ identity.send_magic_link
109
+ redirect_to new_session_path, notice: "Check your email for a sign-in link"
110
+ else
111
+ redirect_to new_session_path, alert: "No account found with that email"
112
+ end
113
+ end
114
+
115
+ def destroy
116
+ terminate_session
117
+ redirect_to root_path
118
+ end
119
+ end
120
+ ```
121
+
122
+ ### Magic Links Controller
123
+
124
+ ```ruby
125
+ class Sessions::MagicLinksController < ApplicationController
126
+ allow_unauthenticated_access
127
+
128
+ def show
129
+ if magic_link = MagicLink.authenticate(params[:code])
130
+ start_new_session_for(magic_link.identity)
131
+ redirect_to session.delete(:return_to) || root_path, notice: "Signed in successfully"
132
+ else
133
+ redirect_to new_session_path, alert: "Invalid or expired link"
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ ## Testing
140
+
141
+ ```ruby
142
+ # test/models/identity_test.rb
143
+ class IdentityTest < ActiveSupport::TestCase
144
+ test "normalizes email address to lowercase" do
145
+ identity = Identity.create!(email_address: "TEST@EXAMPLE.COM")
146
+ assert_equal "test@example.com", identity.email_address
147
+ end
148
+
149
+ test "validates email format" do
150
+ identity = Identity.new(email_address: "invalid")
151
+ assert_not identity.valid?
152
+ assert_includes identity.errors[:email_address], "is invalid"
153
+ end
154
+
155
+ test "sends magic link" do
156
+ identity = identities(:david)
157
+
158
+ assert_difference -> { identity.magic_links.count }, 1 do
159
+ assert_enqueued_emails 1 do
160
+ identity.send_magic_link
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ # test/models/magic_link_test.rb
167
+ class MagicLinkTest < ActiveSupport::TestCase
168
+ test "generates 6-character code" do
169
+ magic_link = MagicLink.create!(identity: identities(:david))
170
+ assert_equal 6, magic_link.code.length
171
+ assert_match(/\A[A-Z0-9]+\z/, magic_link.code)
172
+ end
173
+
174
+ test "expires after 15 minutes" do
175
+ magic_link = MagicLink.create!(identity: identities(:david))
176
+ assert magic_link.valid_for_use?
177
+
178
+ travel 16.minutes do
179
+ assert magic_link.expired?
180
+ assert_not magic_link.valid_for_use?
181
+ end
182
+ end
183
+
184
+ test "authenticates with valid code" do
185
+ magic_link = MagicLink.create!(identity: identities(:david))
186
+ authenticated = MagicLink.authenticate(magic_link.code)
187
+
188
+ assert_equal magic_link, authenticated
189
+ assert authenticated.used?
190
+ end
191
+
192
+ test "does not authenticate used codes" do
193
+ magic_link = MagicLink.create!(identity: identities(:david))
194
+ MagicLink.authenticate(magic_link.code)
195
+ assert_nil MagicLink.authenticate(magic_link.code)
196
+ end
197
+ end
198
+
199
+ # test/controllers/sessions_controller_test.rb
200
+ class SessionsControllerTest < ActionDispatch::IntegrationTest
201
+ test "create sends magic link" do
202
+ identity = identities(:david)
203
+
204
+ assert_enqueued_emails 1 do
205
+ post session_path, params: { email_address: identity.email_address }
206
+ end
207
+
208
+ assert_redirected_to new_session_path
209
+ end
210
+
211
+ test "destroy terminates session" do
212
+ sign_in_as identities(:david)
213
+ delete session_path
214
+
215
+ assert_redirected_to root_path
216
+ assert_nil cookies[:session_token]
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### Test Helper
222
+
223
+ ```ruby
224
+ # test/test_helper.rb
225
+ class ActionDispatch::IntegrationTest
226
+ def sign_in_as(identity)
227
+ session_record = identity.sessions.create!
228
+ cookies.signed[:session_token] = session_record.token
229
+ end
230
+
231
+ def sign_out
232
+ cookies.delete(:session_token)
233
+ end
234
+ end
235
+ ```
236
+
237
+ ## Security
238
+
239
+ - Use signed cookies with `httponly: true` and `same_site: :lax`
240
+ - Magic links expire in 15 minutes
241
+ - Magic links are one-time use
242
+ - Rate limit login attempts
243
+ - Clean up old sessions with a recurring job
244
+
245
+ ```ruby
246
+ # app/jobs/session_cleanup_job.rb
247
+ class SessionCleanupJob < ApplicationJob
248
+ def perform
249
+ Session.where("created_at < ?", 30.days.ago).delete_all
250
+ MagicLink.where("expires_at < ?", 1.day.ago).delete_all
251
+ end
252
+ end
253
+ ```
@@ -0,0 +1,201 @@
1
+ # Session Management Reference
2
+
3
+ ## Session Model
4
+
5
+ ```ruby
6
+ # app/models/session.rb
7
+ class Session < ApplicationRecord
8
+ belongs_to :user
9
+
10
+ before_create :generate_token
11
+ before_create :set_metadata
12
+
13
+ scope :active, -> { where('created_at > ?', 30.days.ago) }
14
+ scope :expired, -> { where('created_at <= ?', 30.days.ago) }
15
+
16
+ def expired?
17
+ created_at <= 30.days.ago
18
+ end
19
+
20
+ private
21
+
22
+ def generate_token
23
+ self.token = SecureRandom.urlsafe_base64(32)
24
+ end
25
+
26
+ def set_metadata
27
+ self.ip_address = Current.ip_address
28
+ self.user_agent = Current.user_agent
29
+ end
30
+ end
31
+ ```
32
+
33
+ ## Session Table Schema
34
+
35
+ ```ruby
36
+ # db/migrate/xxx_create_sessions.rb
37
+ class CreateSessions < ActiveRecord::Migration[8.0]
38
+ def change
39
+ create_table :sessions do |t|
40
+ t.references :user, null: false, foreign_key: true
41
+ t.string :token, null: false
42
+ t.string :ip_address
43
+ t.string :user_agent
44
+ t.timestamps
45
+ end
46
+
47
+ add_index :sessions, :token, unique: true
48
+ end
49
+ end
50
+ ```
51
+
52
+ ## Cookie Security
53
+
54
+ ### Secure Cookie Settings
55
+
56
+ ```ruby
57
+ def start_new_session_for(user)
58
+ session = user.sessions.create!
59
+
60
+ cookies.signed.permanent[:session_token] = {
61
+ value: session.token,
62
+ httponly: true, # JavaScript can't access
63
+ secure: Rails.env.production?, # HTTPS only in production
64
+ same_site: :lax # CSRF protection
65
+ }
66
+
67
+ Current.session = session
68
+ end
69
+ ```
70
+
71
+ ### Cookie Options
72
+
73
+ | Option | Purpose | Value |
74
+ |--------|---------|-------|
75
+ | `httponly` | Prevent XSS access | `true` |
76
+ | `secure` | HTTPS only | `true` in production |
77
+ | `same_site` | CSRF protection | `:lax` or `:strict` |
78
+ | `expires` | Cookie lifetime | `2.weeks.from_now` |
79
+ | `domain` | Cookie scope | `.example.com` for subdomains |
80
+
81
+ ## Session Lifecycle
82
+
83
+ ### Starting Session
84
+
85
+ ```ruby
86
+ def start_new_session_for(user)
87
+ # Terminate existing sessions if desired
88
+ # user.sessions.destroy_all
89
+
90
+ session = user.sessions.create!
91
+ cookies.signed.permanent[:session_token] = {
92
+ value: session.token,
93
+ httponly: true
94
+ }
95
+ Current.session = session
96
+ end
97
+ ```
98
+
99
+ ### Resuming Session
100
+
101
+ ```ruby
102
+ def resume_session
103
+ return unless (token = cookies.signed[:session_token])
104
+ return unless (session = Session.find_by_token(token))
105
+ return if session.expired?
106
+
107
+ # Update last seen
108
+ session.touch(:last_seen_at)
109
+ Current.session = session
110
+ end
111
+ ```
112
+
113
+ ### Terminating Session
114
+
115
+ ```ruby
116
+ def terminate_session
117
+ Current.session&.destroy
118
+ cookies.delete(:session_token)
119
+ reset_session # Clear Rails session too
120
+ end
121
+ ```
122
+
123
+ ## Multiple Device Sessions
124
+
125
+ ### Viewing Active Sessions
126
+
127
+ ```ruby
128
+ # app/controllers/sessions_controller.rb
129
+ def index
130
+ @sessions = Current.user.sessions.active.order(created_at: :desc)
131
+ @current_session = Current.session
132
+ end
133
+ ```
134
+
135
+ ```erb
136
+ <%# app/views/sessions/index.html.erb %>
137
+ <h2>Active Sessions</h2>
138
+
139
+ <% @sessions.each do |session| %>
140
+ <div class="session <%= 'current' if session == @current_session %>">
141
+ <p><%= session.ip_address %></p>
142
+ <p><%= session.user_agent %></p>
143
+ <p>Started: <%= time_ago_in_words(session.created_at) %> ago</p>
144
+
145
+ <% unless session == @current_session %>
146
+ <%= button_to "Revoke", session_path(session), method: :delete %>
147
+ <% end %>
148
+ </div>
149
+ <% end %>
150
+
151
+ <%= button_to "Sign out all other devices",
152
+ revoke_all_sessions_path, method: :post %>
153
+ ```
154
+
155
+ ### Revoking Other Sessions
156
+
157
+ ```ruby
158
+ # app/controllers/sessions_controller.rb
159
+ def revoke_all
160
+ Current.user.sessions.where.not(id: Current.session.id).destroy_all
161
+ redirect_to sessions_path, notice: "All other sessions terminated"
162
+ end
163
+ ```
164
+
165
+ ## Session Cleanup
166
+
167
+ ### Scheduled Cleanup Job
168
+
169
+ ```ruby
170
+ # app/jobs/cleanup_expired_sessions_job.rb
171
+ class CleanupExpiredSessionsJob < ApplicationJob
172
+ queue_as :low
173
+
174
+ def perform
175
+ Session.expired.delete_all
176
+ end
177
+ end
178
+
179
+ # config/recurring.yml
180
+ cleanup_sessions:
181
+ class: CleanupExpiredSessionsJob
182
+ schedule: every day at 3am
183
+ ```
184
+
185
+ ## Security Considerations
186
+
187
+ 1. **Token Rotation**: Regenerate token after password change
188
+ 2. **IP Binding**: Optional - bind session to IP address
189
+ 3. **User Agent Tracking**: Detect suspicious changes
190
+ 4. **Concurrent Session Limits**: Limit active sessions per user
191
+ 5. **Session Timeout**: Expire inactive sessions
192
+
193
+ ```ruby
194
+ # Rotate token on sensitive actions
195
+ def rotate_session_token
196
+ new_session = Current.user.sessions.create!
197
+ Current.session.destroy
198
+ cookies.signed.permanent[:session_token] = new_session.token
199
+ Current.session = new_session
200
+ end
201
+ ```