rubyn-code 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/LICENSE +21 -0
- data/README.md +620 -0
- data/db/migrations/000_create_schema_migrations.sql +4 -0
- data/db/migrations/001_create_sessions.sql +16 -0
- data/db/migrations/002_create_messages.sql +16 -0
- data/db/migrations/003_create_tasks.sql +17 -0
- data/db/migrations/004_create_task_dependencies.sql +8 -0
- data/db/migrations/005_create_memories.sql +44 -0
- data/db/migrations/006_create_cost_records.sql +16 -0
- data/db/migrations/007_create_hooks.sql +12 -0
- data/db/migrations/008_create_skills_cache.sql +8 -0
- data/db/migrations/009_create_teams.sql +27 -0
- data/db/migrations/010_create_instincts.sql +15 -0
- data/exe/rubyn-code +6 -0
- data/lib/rubyn_code/agent/conversation.rb +193 -0
- data/lib/rubyn_code/agent/loop.rb +517 -0
- data/lib/rubyn_code/agent/loop_detector.rb +78 -0
- data/lib/rubyn_code/auth/oauth.rb +174 -0
- data/lib/rubyn_code/auth/server.rb +126 -0
- data/lib/rubyn_code/auth/token_store.rb +153 -0
- data/lib/rubyn_code/autonomous/daemon.rb +233 -0
- data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
- data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
- data/lib/rubyn_code/background/job.rb +19 -0
- data/lib/rubyn_code/background/notifier.rb +44 -0
- data/lib/rubyn_code/background/worker.rb +146 -0
- data/lib/rubyn_code/cli/app.rb +118 -0
- data/lib/rubyn_code/cli/input_handler.rb +79 -0
- data/lib/rubyn_code/cli/renderer.rb +205 -0
- data/lib/rubyn_code/cli/repl.rb +519 -0
- data/lib/rubyn_code/cli/spinner.rb +100 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
- data/lib/rubyn_code/config/defaults.rb +43 -0
- data/lib/rubyn_code/config/project_config.rb +120 -0
- data/lib/rubyn_code/config/settings.rb +127 -0
- data/lib/rubyn_code/context/auto_compact.rb +81 -0
- data/lib/rubyn_code/context/compactor.rb +89 -0
- data/lib/rubyn_code/context/manager.rb +91 -0
- data/lib/rubyn_code/context/manual_compact.rb +87 -0
- data/lib/rubyn_code/context/micro_compact.rb +135 -0
- data/lib/rubyn_code/db/connection.rb +176 -0
- data/lib/rubyn_code/db/migrator.rb +146 -0
- data/lib/rubyn_code/db/schema.rb +106 -0
- data/lib/rubyn_code/hooks/built_in.rb +124 -0
- data/lib/rubyn_code/hooks/registry.rb +99 -0
- data/lib/rubyn_code/hooks/runner.rb +88 -0
- data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
- data/lib/rubyn_code/learning/extractor.rb +191 -0
- data/lib/rubyn_code/learning/injector.rb +138 -0
- data/lib/rubyn_code/learning/instinct.rb +172 -0
- data/lib/rubyn_code/llm/client.rb +218 -0
- data/lib/rubyn_code/llm/message_builder.rb +116 -0
- data/lib/rubyn_code/llm/streaming.rb +203 -0
- data/lib/rubyn_code/mcp/client.rb +139 -0
- data/lib/rubyn_code/mcp/config.rb +83 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
- data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
- data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
- data/lib/rubyn_code/memory/models.rb +62 -0
- data/lib/rubyn_code/memory/search.rb +181 -0
- data/lib/rubyn_code/memory/session_persistence.rb +194 -0
- data/lib/rubyn_code/memory/store.rb +199 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
- data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
- data/lib/rubyn_code/observability/models.rb +29 -0
- data/lib/rubyn_code/observability/token_counter.rb +42 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
- data/lib/rubyn_code/output/diff_renderer.rb +212 -0
- data/lib/rubyn_code/output/formatter.rb +120 -0
- data/lib/rubyn_code/permissions/deny_list.rb +49 -0
- data/lib/rubyn_code/permissions/policy.rb +59 -0
- data/lib/rubyn_code/permissions/prompter.rb +80 -0
- data/lib/rubyn_code/permissions/tier.rb +22 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
- data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
- data/lib/rubyn_code/skills/catalog.rb +70 -0
- data/lib/rubyn_code/skills/document.rb +80 -0
- data/lib/rubyn_code/skills/loader.rb +57 -0
- data/lib/rubyn_code/sub_agents/runner.rb +168 -0
- data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
- data/lib/rubyn_code/tasks/dag.rb +208 -0
- data/lib/rubyn_code/tasks/manager.rb +212 -0
- data/lib/rubyn_code/tasks/models.rb +31 -0
- data/lib/rubyn_code/teams/mailbox.rb +128 -0
- data/lib/rubyn_code/teams/manager.rb +175 -0
- data/lib/rubyn_code/teams/teammate.rb +38 -0
- data/lib/rubyn_code/tools/background_run.rb +41 -0
- data/lib/rubyn_code/tools/base.rb +84 -0
- data/lib/rubyn_code/tools/bash.rb +81 -0
- data/lib/rubyn_code/tools/bundle_add.rb +53 -0
- data/lib/rubyn_code/tools/bundle_install.rb +41 -0
- data/lib/rubyn_code/tools/compact.rb +57 -0
- data/lib/rubyn_code/tools/db_migrate.rb +52 -0
- data/lib/rubyn_code/tools/edit_file.rb +49 -0
- data/lib/rubyn_code/tools/executor.rb +62 -0
- data/lib/rubyn_code/tools/git_commit.rb +97 -0
- data/lib/rubyn_code/tools/git_diff.rb +61 -0
- data/lib/rubyn_code/tools/git_log.rb +59 -0
- data/lib/rubyn_code/tools/git_status.rb +59 -0
- data/lib/rubyn_code/tools/glob.rb +44 -0
- data/lib/rubyn_code/tools/grep.rb +81 -0
- data/lib/rubyn_code/tools/load_skill.rb +41 -0
- data/lib/rubyn_code/tools/memory_search.rb +77 -0
- data/lib/rubyn_code/tools/memory_write.rb +52 -0
- data/lib/rubyn_code/tools/rails_generate.rb +54 -0
- data/lib/rubyn_code/tools/read_file.rb +38 -0
- data/lib/rubyn_code/tools/read_inbox.rb +64 -0
- data/lib/rubyn_code/tools/registry.rb +48 -0
- data/lib/rubyn_code/tools/review_pr.rb +145 -0
- data/lib/rubyn_code/tools/run_specs.rb +75 -0
- data/lib/rubyn_code/tools/schema.rb +59 -0
- data/lib/rubyn_code/tools/send_message.rb +53 -0
- data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
- data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
- data/lib/rubyn_code/tools/task.rb +148 -0
- data/lib/rubyn_code/tools/web_fetch.rb +108 -0
- data/lib/rubyn_code/tools/web_search.rb +196 -0
- data/lib/rubyn_code/tools/write_file.rb +30 -0
- data/lib/rubyn_code/version.rb +5 -0
- data/lib/rubyn_code.rb +203 -0
- data/skills/code_quality/fits_in_your_head.md +189 -0
- data/skills/code_quality/naming_conventions.md +213 -0
- data/skills/code_quality/null_object.md +205 -0
- data/skills/code_quality/technical_debt.md +135 -0
- data/skills/code_quality/value_objects.md +216 -0
- data/skills/code_quality/yagni.md +176 -0
- data/skills/design_patterns/adapter.md +191 -0
- data/skills/design_patterns/bridge_memento_visitor.md +254 -0
- data/skills/design_patterns/builder.md +158 -0
- data/skills/design_patterns/command.md +126 -0
- data/skills/design_patterns/composite.md +147 -0
- data/skills/design_patterns/decorator.md +204 -0
- data/skills/design_patterns/facade.md +133 -0
- data/skills/design_patterns/factory_method.md +169 -0
- data/skills/design_patterns/iterator.md +116 -0
- data/skills/design_patterns/mediator.md +133 -0
- data/skills/design_patterns/observer.md +177 -0
- data/skills/design_patterns/proxy.md +140 -0
- data/skills/design_patterns/singleton.md +124 -0
- data/skills/design_patterns/state.md +207 -0
- data/skills/design_patterns/strategy.md +127 -0
- data/skills/design_patterns/template_method.md +173 -0
- data/skills/gems/devise.md +365 -0
- data/skills/gems/dry_rb.md +186 -0
- data/skills/gems/factory_bot.md +268 -0
- data/skills/gems/faraday.md +263 -0
- data/skills/gems/graphql_ruby.md +514 -0
- data/skills/gems/pundit.md +446 -0
- data/skills/gems/redis.md +219 -0
- data/skills/gems/rubocop.md +257 -0
- data/skills/gems/sidekiq.md +360 -0
- data/skills/gems/stripe.md +224 -0
- data/skills/minitest/assertions.md +185 -0
- data/skills/minitest/fixtures.md +238 -0
- data/skills/minitest/integration_tests.md +210 -0
- data/skills/minitest/mailers_and_jobs.md +218 -0
- data/skills/minitest/mocking_stubbing.md +202 -0
- data/skills/minitest/service_tests_and_performance.md +246 -0
- data/skills/minitest/structure_and_conventions.md +169 -0
- data/skills/minitest/system_tests.md +237 -0
- data/skills/rails/action_cable.md +160 -0
- data/skills/rails/active_record_basics.md +174 -0
- data/skills/rails/active_storage.md +242 -0
- data/skills/rails/api_design.md +212 -0
- data/skills/rails/associations.md +182 -0
- data/skills/rails/background_jobs.md +212 -0
- data/skills/rails/caching.md +158 -0
- data/skills/rails/callbacks.md +135 -0
- data/skills/rails/concerns_controllers.md +218 -0
- data/skills/rails/concerns_models.md +280 -0
- data/skills/rails/controllers.md +190 -0
- data/skills/rails/engines.md +201 -0
- data/skills/rails/form_objects.md +168 -0
- data/skills/rails/hotwire.md +229 -0
- data/skills/rails/internationalization.md +192 -0
- data/skills/rails/logging.md +198 -0
- data/skills/rails/mailers.md +180 -0
- data/skills/rails/migrations.md +200 -0
- data/skills/rails/multitenancy.md +207 -0
- data/skills/rails/n_plus_one.md +151 -0
- data/skills/rails/presenters.md +244 -0
- data/skills/rails/query_objects.md +177 -0
- data/skills/rails/routing.md +194 -0
- data/skills/rails/scopes.md +187 -0
- data/skills/rails/security.md +233 -0
- data/skills/rails/serializers.md +243 -0
- data/skills/rails/service_objects.md +184 -0
- data/skills/rails/testing_strategy.md +258 -0
- data/skills/rails/validations.md +206 -0
- data/skills/refactoring/code_smells.md +251 -0
- data/skills/refactoring/command_query_separation.md +166 -0
- data/skills/refactoring/encapsulate_collection.md +125 -0
- data/skills/refactoring/extract_class.md +138 -0
- data/skills/refactoring/extract_method.md +185 -0
- data/skills/refactoring/replace_conditional.md +211 -0
- data/skills/refactoring/value_objects.md +246 -0
- data/skills/rspec/build_stubbed.md +199 -0
- data/skills/rspec/factory_design.md +206 -0
- data/skills/rspec/let_vs_let_bang.md +161 -0
- data/skills/rspec/mocking_stubbing.md +209 -0
- data/skills/rspec/request_specs.md +212 -0
- data/skills/rspec/service_specs.md +262 -0
- data/skills/rspec/shared_examples.md +244 -0
- data/skills/rspec/system_specs.md +286 -0
- data/skills/rspec/test_performance.md +215 -0
- data/skills/ruby/blocks_procs_lambdas.md +204 -0
- data/skills/ruby/classes.md +155 -0
- data/skills/ruby/concurrency.md +194 -0
- data/skills/ruby/data_struct_openstruct.md +158 -0
- data/skills/ruby/debugging_profiling.md +204 -0
- data/skills/ruby/enumerable_patterns.md +168 -0
- data/skills/ruby/exception_handling.md +199 -0
- data/skills/ruby/file_io.md +217 -0
- data/skills/ruby/hashes.md +195 -0
- data/skills/ruby/metaprogramming.md +170 -0
- data/skills/ruby/modules.md +210 -0
- data/skills/ruby/pattern_matching.md +177 -0
- data/skills/ruby/regular_expressions.md +166 -0
- data/skills/ruby/result_objects.md +200 -0
- data/skills/ruby/strings.md +177 -0
- data/skills/ruby_project/bundler_dependencies.md +181 -0
- data/skills/ruby_project/cli_tools.md +224 -0
- data/skills/ruby_project/rake_tasks.md +146 -0
- data/skills/ruby_project/structure.md +261 -0
- data/skills/sinatra/application_structure.md +241 -0
- data/skills/sinatra/middleware_and_deployment.md +221 -0
- data/skills/sinatra/testing.md +233 -0
- data/skills/solid/dependency_inversion.md +195 -0
- data/skills/solid/interface_segregation.md +237 -0
- data/skills/solid/liskov_substitution.md +263 -0
- data/skills/solid/open_closed.md +212 -0
- data/skills/solid/single_responsibility.md +183 -0
- metadata +397 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Design Pattern: Strategy
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Define a family of algorithms, put each in its own class, and make them interchangeable. The calling code (context) delegates to a strategy object and doesn't know or care which implementation it gets.
|
|
6
|
+
|
|
7
|
+
In Ruby, strategies can be classes, procs/lambdas, or any object that responds to the expected method — duck typing makes the pattern lightweight.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Strategy as classes — best for complex algorithms with their own state
|
|
11
|
+
|
|
12
|
+
class Credits::PricingStrategy
|
|
13
|
+
def calculate_cost(input_tokens, output_tokens, cache_read_tokens)
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Credits::HaikuPricing < Credits::PricingStrategy
|
|
19
|
+
INPUT_RATE = 1.0 / 1_000_000 # $1 per 1M input tokens
|
|
20
|
+
OUTPUT_RATE = 5.0 / 1_000_000 # $5 per 1M output tokens
|
|
21
|
+
CACHE_RATE = 0.1 / 1_000_000 # $0.10 per 1M cached tokens
|
|
22
|
+
|
|
23
|
+
def calculate_cost(input_tokens, output_tokens, cache_read_tokens)
|
|
24
|
+
(input_tokens * INPUT_RATE) +
|
|
25
|
+
(output_tokens * OUTPUT_RATE) +
|
|
26
|
+
(cache_read_tokens * CACHE_RATE)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Credits::SonnetPricing < Credits::PricingStrategy
|
|
31
|
+
INPUT_RATE = 3.0 / 1_000_000
|
|
32
|
+
OUTPUT_RATE = 15.0 / 1_000_000
|
|
33
|
+
CACHE_RATE = 0.3 / 1_000_000
|
|
34
|
+
|
|
35
|
+
def calculate_cost(input_tokens, output_tokens, cache_read_tokens)
|
|
36
|
+
(input_tokens * INPUT_RATE) +
|
|
37
|
+
(output_tokens * OUTPUT_RATE) +
|
|
38
|
+
(cache_read_tokens * CACHE_RATE)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Context — doesn't know which pricing strategy it's using
|
|
43
|
+
class Credits::DeductionService
|
|
44
|
+
def initialize(pricing: Credits::HaikuPricing.new)
|
|
45
|
+
@pricing = pricing
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(interaction)
|
|
49
|
+
cost = @pricing.calculate_cost(
|
|
50
|
+
interaction.input_tokens,
|
|
51
|
+
interaction.output_tokens,
|
|
52
|
+
interaction.cache_read_tokens
|
|
53
|
+
)
|
|
54
|
+
credits = (cost / Credits::COST_PER_CREDIT).ceil
|
|
55
|
+
|
|
56
|
+
interaction.update!(credits_used: credits, cost_usd: cost)
|
|
57
|
+
interaction.user.deduct_credits!(credits)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Strategy as procs — best for simple, inline algorithms:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class Orders::SortService
|
|
66
|
+
STRATEGIES = {
|
|
67
|
+
newest: ->(scope) { scope.order(created_at: :desc) },
|
|
68
|
+
oldest: ->(scope) { scope.order(created_at: :asc) },
|
|
69
|
+
highest: ->(scope) { scope.order(total: :desc) },
|
|
70
|
+
alphabetical: ->(scope) { scope.joins(:user).order("users.name ASC") }
|
|
71
|
+
}.freeze
|
|
72
|
+
|
|
73
|
+
def call(orders, strategy_name:)
|
|
74
|
+
strategy = STRATEGIES.fetch(strategy_name, STRATEGIES[:newest])
|
|
75
|
+
strategy.call(orders)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Strategy via Ruby blocks — maximum flexibility:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class DataExporter
|
|
84
|
+
def export(records, &formatter)
|
|
85
|
+
records.map { |record| formatter.call(record) }.join("\n")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
exporter = DataExporter.new
|
|
90
|
+
exporter.export(orders) { |o| o.to_json }
|
|
91
|
+
exporter.export(orders) { |o| "#{o.reference},#{o.total}" }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Why This Is Good
|
|
95
|
+
|
|
96
|
+
- **Adding a new algorithm doesn't touch existing code.** Adding `OpusPricing` means writing one new class. `DeductionService`, `HaikuPricing`, and `SonnetPricing` don't change.
|
|
97
|
+
- **Each strategy is independently testable.** Test `HaikuPricing#calculate_cost` with just numbers — no service, no user, no credits.
|
|
98
|
+
- **Runtime swappable.** Pro mode can use `SonnetPricing`, free tier uses `HaikuPricing`, all determined at runtime without conditionals in the service.
|
|
99
|
+
- **Ruby's duck typing makes it lightweight.** No interfaces to declare, no abstract classes to inherit from. Any object with `calculate_cost(input, output, cache)` is a valid strategy.
|
|
100
|
+
|
|
101
|
+
## When To Apply
|
|
102
|
+
|
|
103
|
+
- You have **multiple algorithms for the same task** — pricing models, sorting methods, formatting options, authentication strategies.
|
|
104
|
+
- The algorithm **varies at runtime** based on user input, configuration, or feature flags.
|
|
105
|
+
- You find yourself writing `case/when` or `if/elsif` chains that select different behavior based on a type.
|
|
106
|
+
- You want to **test algorithms in isolation** without the context that uses them.
|
|
107
|
+
|
|
108
|
+
## When NOT To Apply
|
|
109
|
+
|
|
110
|
+
- **Two simple branches that won't grow.** An `if premium?` / `else` is clearer than a strategy pattern for two options that are unlikely to become three.
|
|
111
|
+
- **The "algorithm" is a single line.** `collection.sort_by(&:name)` vs `collection.sort_by(&:created_at)` doesn't need a strategy class — just pass the sort key.
|
|
112
|
+
- **The behavior never varies at runtime.** If the app always uses Haiku pricing and will never use anything else, injecting a strategy adds unnecessary indirection.
|
|
113
|
+
|
|
114
|
+
## Rails Example
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# config/initializers/pricing.rb
|
|
118
|
+
PRICING_STRATEGIES = {
|
|
119
|
+
"haiku" => Credits::HaikuPricing.new,
|
|
120
|
+
"sonnet" => Credits::SonnetPricing.new,
|
|
121
|
+
"opus" => Credits::OpusPricing.new
|
|
122
|
+
}.freeze
|
|
123
|
+
|
|
124
|
+
# Used in the interaction pipeline
|
|
125
|
+
pricing = PRICING_STRATEGIES.fetch(interaction.model_tier, PRICING_STRATEGIES["haiku"])
|
|
126
|
+
Credits::DeductionService.new(pricing: pricing).call(interaction)
|
|
127
|
+
```
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Design Pattern: Template Method
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Define the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm's structure. The base class controls the *what* and *when*; subclasses control the *how*.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Base class defines the algorithm skeleton
|
|
9
|
+
class Reports::BaseReport
|
|
10
|
+
def generate(start_date, end_date)
|
|
11
|
+
data = fetch_data(start_date, end_date)
|
|
12
|
+
filtered = apply_filters(data)
|
|
13
|
+
formatted = format_output(filtered)
|
|
14
|
+
add_metadata(formatted, start_date, end_date)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# Steps that subclasses MUST override
|
|
20
|
+
def fetch_data(start_date, end_date)
|
|
21
|
+
raise NotImplementedError, "#{self.class} must implement #fetch_data"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def format_output(data)
|
|
25
|
+
raise NotImplementedError, "#{self.class} must implement #format_output"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Hook methods — subclasses CAN override, but don't have to
|
|
29
|
+
def apply_filters(data)
|
|
30
|
+
data # Default: no filtering
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_metadata(output, start_date, end_date)
|
|
34
|
+
{
|
|
35
|
+
report_type: self.class.name.demodulize.underscore,
|
|
36
|
+
generated_at: Time.current.iso8601,
|
|
37
|
+
period: "#{start_date} to #{end_date}",
|
|
38
|
+
data: output
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Subclass: Revenue report
|
|
44
|
+
class Reports::RevenueReport < Reports::BaseReport
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def fetch_data(start_date, end_date)
|
|
48
|
+
Order.where(created_at: start_date..end_date)
|
|
49
|
+
.group(:status)
|
|
50
|
+
.sum(:total)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def format_output(data)
|
|
54
|
+
data.map { |status, total| { status: status, total: total.round(2) } }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Subclass: User activity report with custom filtering
|
|
59
|
+
class Reports::UserActivityReport < Reports::BaseReport
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def fetch_data(start_date, end_date)
|
|
63
|
+
User.where(last_active_at: start_date..end_date)
|
|
64
|
+
.select(:id, :email, :last_active_at, :plan)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def apply_filters(data)
|
|
68
|
+
data.where.not(plan: "free") # Override hook: exclude free users
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_output(data)
|
|
72
|
+
data.map { |u| { email: u.email, plan: u.plan, last_active: u.last_active_at.iso8601 } }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Subclass: Credit usage report
|
|
77
|
+
class Reports::CreditUsageReport < Reports::BaseReport
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def fetch_data(start_date, end_date)
|
|
81
|
+
CreditLedger.where(created_at: start_date..end_date)
|
|
82
|
+
.joins(:user)
|
|
83
|
+
.group("users.email")
|
|
84
|
+
.sum(:amount)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_output(data)
|
|
88
|
+
data.sort_by { |_, amount| amount }
|
|
89
|
+
.map { |email, amount| { email: email, credits_used: amount.abs } }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Usage — all reports follow the same algorithm, different data/formatting
|
|
94
|
+
revenue = Reports::RevenueReport.new.generate(30.days.ago, Date.today)
|
|
95
|
+
activity = Reports::UserActivityReport.new.generate(7.days.ago, Date.today)
|
|
96
|
+
credits = Reports::CreditUsageReport.new.generate(1.month.ago.beginning_of_month, 1.month.ago.end_of_month)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Why This Is Good
|
|
100
|
+
|
|
101
|
+
- **Algorithm is defined once.** The sequence — fetch, filter, format, add metadata — lives in `BaseReport`. No subclass can accidentally skip the metadata step or reorder the operations.
|
|
102
|
+
- **Variation without duplication.** Each report only implements what's different (data source, formatting). The shared steps (metadata, the overall flow) are inherited.
|
|
103
|
+
- **Hook methods provide optional customization.** `apply_filters` has a default (no-op). Subclasses override it only when they need filtering. No empty method stubs needed.
|
|
104
|
+
- **New reports are easy.** Create a new subclass, implement `fetch_data` and `format_output`, done. The algorithm skeleton works automatically.
|
|
105
|
+
|
|
106
|
+
## Anti-Pattern
|
|
107
|
+
|
|
108
|
+
Copy-pasting the algorithm into each report class:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class RevenueReport
|
|
112
|
+
def generate(start_date, end_date)
|
|
113
|
+
data = Order.where(created_at: start_date..end_date).group(:status).sum(:total)
|
|
114
|
+
formatted = data.map { |status, total| { status: status, total: total } }
|
|
115
|
+
{ report_type: "revenue", generated_at: Time.current.iso8601, period: "#{start_date} to #{end_date}", data: formatted }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class UserActivityReport
|
|
120
|
+
def generate(start_date, end_date)
|
|
121
|
+
data = User.where(last_active_at: start_date..end_date)
|
|
122
|
+
formatted = data.map { |u| { email: u.email, last_active: u.last_active_at } }
|
|
123
|
+
{ report_type: "user_activity", generated_at: Time.current.iso8601, period: "#{start_date} to #{end_date}", data: formatted }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The metadata hash is duplicated in every report. Changing the metadata format means editing every class.
|
|
129
|
+
|
|
130
|
+
## When To Apply
|
|
131
|
+
|
|
132
|
+
- **Multiple classes follow the same algorithm with different details.** Reports, importers, exporters, notification handlers, data processors.
|
|
133
|
+
- **You want to enforce a sequence of steps.** The base class guarantees that filtering always happens after fetching and before formatting.
|
|
134
|
+
- **Common behavior + specific behavior.** Metadata generation is common. Data fetching is specific.
|
|
135
|
+
|
|
136
|
+
## When NOT To Apply
|
|
137
|
+
|
|
138
|
+
- **Two classes with minor differences.** If only `fetch_data` varies and everything else is identical, a single class with an injected strategy (proc or data source object) is simpler than inheritance.
|
|
139
|
+
- **Ruby modules might be better.** If you need to mix the template into unrelated class hierarchies, use a module with a template method instead of inheritance.
|
|
140
|
+
- **Don't force inheritance for code reuse.** If the subclasses don't have a genuine "is-a" relationship, prefer composition (Strategy pattern) over inheritance (Template Method).
|
|
141
|
+
|
|
142
|
+
## Edge Cases
|
|
143
|
+
|
|
144
|
+
**Template Method via modules (no inheritance needed):**
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
module Importable
|
|
148
|
+
def import(file_path)
|
|
149
|
+
rows = parse(file_path)
|
|
150
|
+
validated = rows.select { |row| valid?(row) }
|
|
151
|
+
validated.each { |row| persist(row) }
|
|
152
|
+
{ imported: validated.size, rejected: rows.size - validated.size }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def parse(file_path) = raise(NotImplementedError)
|
|
158
|
+
def valid?(row) = true # Hook: override to add validation
|
|
159
|
+
def persist(row) = raise(NotImplementedError)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
class CsvOrderImporter
|
|
163
|
+
include Importable
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def parse(file_path) = CSV.read(file_path, headers: true).map(&:to_h)
|
|
168
|
+
def valid?(row) = row["total"].to_f > 0
|
|
169
|
+
def persist(row) = Order.create!(row)
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This avoids class inheritance while still providing the template method's algorithmic skeleton.
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Gem: Devise
|
|
2
|
+
|
|
3
|
+
## What It Is
|
|
4
|
+
|
|
5
|
+
Devise is the standard Rails authentication gem. It handles registration, login, logout, password reset, email confirmation, account locking, and session management. It's built on Warden (Rack middleware) and provides generators, routes, views, and controllers out of the box.
|
|
6
|
+
|
|
7
|
+
## Setup Done Right
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem 'devise'
|
|
12
|
+
|
|
13
|
+
# After bundle install
|
|
14
|
+
rails generate devise:install
|
|
15
|
+
rails generate devise User
|
|
16
|
+
rails db:migrate
|
|
17
|
+
|
|
18
|
+
# config/initializers/devise.rb — the settings that matter
|
|
19
|
+
Devise.setup do |config|
|
|
20
|
+
config.mailer_sender = 'noreply@rubyn.ai'
|
|
21
|
+
|
|
22
|
+
# IMPORTANT: Set these in production
|
|
23
|
+
config.pepper = ENV.fetch('DEVISE_PEPPER') # Extra layer on bcrypt
|
|
24
|
+
config.secret_key = ENV.fetch('DEVISE_SECRET_KEY') # For token generation
|
|
25
|
+
|
|
26
|
+
# Password requirements
|
|
27
|
+
config.password_length = 8..128
|
|
28
|
+
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ # Simpler, less false rejections
|
|
29
|
+
|
|
30
|
+
# Lockable — lock after failed attempts
|
|
31
|
+
config.lock_strategy = :failed_attempts
|
|
32
|
+
config.unlock_strategy = :both # Email + time
|
|
33
|
+
config.maximum_attempts = 5
|
|
34
|
+
config.unlock_in = 1.hour
|
|
35
|
+
|
|
36
|
+
# Confirmable — if you use it
|
|
37
|
+
config.confirm_within = 3.days
|
|
38
|
+
config.reconfirmable = true
|
|
39
|
+
|
|
40
|
+
# Rememberable
|
|
41
|
+
config.remember_for = 2.weeks
|
|
42
|
+
config.extend_remember_period = true # Resets timer on each visit
|
|
43
|
+
|
|
44
|
+
# Timeoutable — session timeout
|
|
45
|
+
config.timeout_in = 30.minutes
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Gotcha #1: Strong Parameters
|
|
50
|
+
|
|
51
|
+
Devise uses its own parameter sanitizer, NOT standard Rails strong params. If you add fields to the User model (like `name`), they'll be silently dropped unless you configure the sanitizer.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# WRONG: This does nothing for Devise actions
|
|
55
|
+
class UsersController < ApplicationController
|
|
56
|
+
def user_params
|
|
57
|
+
params.require(:user).permit(:email, :password, :name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# RIGHT: Configure in ApplicationController
|
|
62
|
+
class ApplicationController < ActionController::Base
|
|
63
|
+
before_action :configure_permitted_parameters, if: :devise_controller?
|
|
64
|
+
|
|
65
|
+
protected
|
|
66
|
+
|
|
67
|
+
def configure_permitted_parameters
|
|
68
|
+
# sign_up: registration#create
|
|
69
|
+
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :company_name])
|
|
70
|
+
|
|
71
|
+
# account_update: registration#update
|
|
72
|
+
devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar])
|
|
73
|
+
|
|
74
|
+
# sign_in: session#create (rarely needed)
|
|
75
|
+
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**The trap:** You add a `name` field to the registration form, it submits correctly, but the name is never saved. No error, no warning — Devise silently drops unpermitted params. Check the server logs for "Unpermitted parameter: :name".
|
|
81
|
+
|
|
82
|
+
## Gotcha #2: Customizing Controllers
|
|
83
|
+
|
|
84
|
+
When you override a Devise controller, you MUST tell the router to use your controller, AND you must call `super` or replicate Devise's internal flow correctly.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Generate custom controllers
|
|
88
|
+
rails generate devise:controllers users -c=registrations sessions
|
|
89
|
+
|
|
90
|
+
# config/routes.rb — MUST point to your controllers
|
|
91
|
+
devise_for :users, controllers: {
|
|
92
|
+
registrations: 'users/registrations',
|
|
93
|
+
sessions: 'users/sessions'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# app/controllers/users/registrations_controller.rb
|
|
97
|
+
class Users::RegistrationsController < Devise::RegistrationsController
|
|
98
|
+
# CORRECT: Call super and add your logic around it
|
|
99
|
+
def create
|
|
100
|
+
super do |user|
|
|
101
|
+
# This block runs after the user is built but before redirect
|
|
102
|
+
if user.persisted?
|
|
103
|
+
Projects::CreateDefaultService.call(user)
|
|
104
|
+
WelcomeMailer.welcome(user).deliver_later
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# WRONG: Completely reimplementing create without understanding Devise's flow
|
|
110
|
+
# def create
|
|
111
|
+
# @user = User.new(user_params)
|
|
112
|
+
# if @user.save
|
|
113
|
+
# redirect_to root_path
|
|
114
|
+
# # Missing: sign_in, flash, respond_with, location, etc.
|
|
115
|
+
# end
|
|
116
|
+
# end
|
|
117
|
+
|
|
118
|
+
protected
|
|
119
|
+
|
|
120
|
+
# Where to redirect after signup
|
|
121
|
+
def after_sign_up_path_for(resource)
|
|
122
|
+
dashboard_path
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Where to redirect after update
|
|
126
|
+
def after_update_path_for(resource)
|
|
127
|
+
edit_user_registration_path
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**The trap:** You override `create` without calling `super`. Sign-up "works" but: the user isn't signed in, the flash message is missing, the Warden session isn't set correctly, `current_user` returns nil on the next page, and Turbo/Hotwire breaks because Devise's `respond_with` isn't called.
|
|
133
|
+
|
|
134
|
+
## Gotcha #3: `current_user` Is Nil in Unexpected Places
|
|
135
|
+
|
|
136
|
+
`current_user` relies on Warden middleware. It's not available in models, service objects, mailers, or background jobs.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# WRONG: current_user in a model
|
|
140
|
+
class Order < ApplicationRecord
|
|
141
|
+
before_create :set_creator
|
|
142
|
+
def set_creator
|
|
143
|
+
self.created_by = current_user # NoMethodError — models don't have current_user
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# RIGHT: Use Current attributes or pass the user explicitly
|
|
148
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
149
|
+
attribute :user
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Set in ApplicationController
|
|
153
|
+
class ApplicationController < ActionController::Base
|
|
154
|
+
before_action :set_current_user
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def set_current_user
|
|
159
|
+
Current.user = current_user
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Now available everywhere in the request cycle (but NOT in background jobs)
|
|
164
|
+
class Order < ApplicationRecord
|
|
165
|
+
before_create :set_creator
|
|
166
|
+
def set_creator
|
|
167
|
+
self.created_by = Current.user&.id
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**The trap:** `Current.user` is request-scoped. In Sidekiq jobs, it's nil. Always pass user_id explicitly to background jobs.
|
|
173
|
+
|
|
174
|
+
## Gotcha #4: Password Change Requires Current Password
|
|
175
|
+
|
|
176
|
+
By default, Devise requires `current_password` for any registration update. This catches people when building profile edit pages.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# The form MUST include current_password for updates
|
|
180
|
+
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), method: :put) do |f| %>
|
|
181
|
+
<%= f.text_field :name %>
|
|
182
|
+
<%= f.email_field :email %>
|
|
183
|
+
|
|
184
|
+
<%# THIS IS REQUIRED or the update silently fails %>
|
|
185
|
+
<%= f.password_field :current_password, autocomplete: "current-password" %>
|
|
186
|
+
|
|
187
|
+
<%= f.submit "Update" %>
|
|
188
|
+
<% end %>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If you want to update profile fields WITHOUT requiring the password:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
class Users::RegistrationsController < Devise::RegistrationsController
|
|
195
|
+
protected
|
|
196
|
+
|
|
197
|
+
# Allow update without password when not changing email/password
|
|
198
|
+
def update_resource(resource, params)
|
|
199
|
+
if params[:password].blank? && params[:password_confirmation].blank?
|
|
200
|
+
params.delete(:password)
|
|
201
|
+
params.delete(:password_confirmation)
|
|
202
|
+
params.delete(:current_password)
|
|
203
|
+
resource.update(params)
|
|
204
|
+
else
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Gotcha #5: Turbo/Hotwire Compatibility (Rails 7+)
|
|
212
|
+
|
|
213
|
+
Devise was built before Turbo. Without configuration, failed login/signup forms break because Devise returns HTTP 200 (which Turbo interprets as success) instead of 422.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# config/initializers/devise.rb
|
|
217
|
+
Devise.setup do |config|
|
|
218
|
+
# Rails 7+ with Turbo: Devise must return proper error status codes
|
|
219
|
+
config.responder.error_status = :unprocessable_entity # 422 for validation failures
|
|
220
|
+
config.responder.redirect_status = :see_other # 303 for redirects
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# If you're on Devise < 4.9, you need this in ApplicationController:
|
|
224
|
+
class ApplicationController < ActionController::Base
|
|
225
|
+
class Responder < ActionController::Responder
|
|
226
|
+
def to_turbo_stream
|
|
227
|
+
controller.render(options.merge(formats: :html))
|
|
228
|
+
rescue ActionView::MissingTemplate => e
|
|
229
|
+
if get?
|
|
230
|
+
raise e
|
|
231
|
+
elsif has_errors? && default_action
|
|
232
|
+
render rendering_options.merge(formats: :html, status: :unprocessable_entity)
|
|
233
|
+
else
|
|
234
|
+
redirect_to navigation_location
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
self.responder = Responder
|
|
240
|
+
respond_to :html, :turbo_stream
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**The trap:** You submit a login form with wrong credentials. The page appears to do nothing — no error messages, no redirect. The form just sits there. The response was actually a 200 with error HTML, but Turbo expected 422 to know it should replace the form.
|
|
245
|
+
|
|
246
|
+
## Gotcha #6: Token Authentication for APIs
|
|
247
|
+
|
|
248
|
+
Devise doesn't ship with token auth. Don't try to hack `authenticate_with_http_token` onto Devise — use a separate strategy.
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# WRONG: Trying to use Devise for API auth
|
|
252
|
+
class Api::BaseController < ActionController::API
|
|
253
|
+
before_action :authenticate_user! # This uses session/cookie — doesn't work for APIs
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# RIGHT: Separate API authentication
|
|
257
|
+
class Api::BaseController < ActionController::API
|
|
258
|
+
before_action :authenticate_api_key!
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def authenticate_api_key!
|
|
263
|
+
token = request.headers["Authorization"]&.remove("Bearer ")
|
|
264
|
+
@current_user = ApiKey.find_by(key_digest: Digest::SHA256.hexdigest(token.to_s))&.user
|
|
265
|
+
head :unauthorized unless @current_user
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def current_user
|
|
269
|
+
@current_user
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
For JWT-based API auth, use `devise-jwt` or `doorkeeper`. Don't roll your own JWT implementation.
|
|
275
|
+
|
|
276
|
+
## Gotcha #7: Testing with Devise
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# spec/support/devise.rb
|
|
280
|
+
RSpec.configure do |config|
|
|
281
|
+
config.include Devise::Test::IntegrationHelpers, type: :request
|
|
282
|
+
config.include Devise::Test::IntegrationHelpers, type: :system
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# In request specs — use sign_in helper
|
|
286
|
+
RSpec.describe "Orders", type: :request do
|
|
287
|
+
let(:user) { create(:user) }
|
|
288
|
+
before { sign_in user }
|
|
289
|
+
|
|
290
|
+
it "lists orders" do
|
|
291
|
+
get orders_path
|
|
292
|
+
expect(response).to have_http_status(:ok)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# WRONG: Trying to use sign_in in a model or service spec
|
|
297
|
+
# sign_in is a request/controller helper — it sets the Warden session
|
|
298
|
+
# In service specs, just pass the user directly
|
|
299
|
+
|
|
300
|
+
# For API specs with token auth — don't use sign_in
|
|
301
|
+
RSpec.describe "API Orders", type: :request do
|
|
302
|
+
let(:user) { create(:user) }
|
|
303
|
+
let(:api_key) { create(:api_key, user: user) }
|
|
304
|
+
|
|
305
|
+
it "requires auth" do
|
|
306
|
+
get "/api/v1/orders"
|
|
307
|
+
expect(response).to have_http_status(:unauthorized)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "works with valid key" do
|
|
311
|
+
get "/api/v1/orders", headers: { "Authorization" => "Bearer #{api_key.raw_key}" }
|
|
312
|
+
expect(response).to have_http_status(:ok)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Gotcha #8: Custom Mailer
|
|
318
|
+
|
|
319
|
+
Devise's default mailer sends plain text from `devise/mailer/`. To customize:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
# Generate views first
|
|
323
|
+
rails generate devise:views
|
|
324
|
+
|
|
325
|
+
# For a fully custom mailer:
|
|
326
|
+
# config/initializers/devise.rb
|
|
327
|
+
config.mailer = 'CustomDeviseMailer'
|
|
328
|
+
|
|
329
|
+
# app/mailers/custom_devise_mailer.rb
|
|
330
|
+
class CustomDeviseMailer < Devise::Mailer
|
|
331
|
+
helper :application
|
|
332
|
+
include Devise::Controllers::UrlHelpers
|
|
333
|
+
layout 'mailer'
|
|
334
|
+
|
|
335
|
+
def reset_password_instructions(record, token, opts = {})
|
|
336
|
+
opts[:subject] = "Reset your Rubyn password"
|
|
337
|
+
super
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def confirmation_instructions(record, token, opts = {})
|
|
341
|
+
opts[:subject] = "Confirm your Rubyn account"
|
|
342
|
+
super
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**The trap:** You create `app/views/devise/mailer/reset_password_instructions.html.erb` but emails still use the old template. Devise caches views — restart the server. If using a custom mailer class, the views should be at `app/views/custom_devise_mailer/`.
|
|
348
|
+
|
|
349
|
+
## Do's and Don'ts Summary
|
|
350
|
+
|
|
351
|
+
**DO:**
|
|
352
|
+
- Set `pepper` and `secret_key` from ENV in production
|
|
353
|
+
- Configure parameter sanitizer for any custom fields
|
|
354
|
+
- Use `after_sign_up_path_for` and `after_sign_in_path_for` for redirects
|
|
355
|
+
- Set Turbo-compatible error/redirect status codes
|
|
356
|
+
- Use `sign_in` helper in request specs
|
|
357
|
+
- Use `Current.user` instead of threading `current_user` through every method call
|
|
358
|
+
|
|
359
|
+
**DON'T:**
|
|
360
|
+
- Don't override Devise controllers without calling `super` or fully understanding the flow
|
|
361
|
+
- Don't use Devise session auth for APIs — use token/JWT auth separately
|
|
362
|
+
- Don't put `current_user` in models, mailers, or jobs — it doesn't exist there
|
|
363
|
+
- Don't forget `current_password` in the update form
|
|
364
|
+
- Don't use `devise :token_authenticatable` — it was removed for security reasons
|
|
365
|
+
- Don't store passwords in ENV or logs — Devise handles hashing, but make sure `filter_parameters` includes `:password`
|