develoz-rails 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.
Files changed (172) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.github/workflows/release.yml +459 -0
  4. data/.rubocop.yml +52 -0
  5. data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
  6. data/.tool-versions +2 -0
  7. data/CHANGELOG.md +28 -0
  8. data/CONTRIBUTING.md +120 -0
  9. data/README.md +76 -0
  10. data/Rakefile +18 -0
  11. data/config/generators.yml +105 -0
  12. data/docs/guide/installation.md +574 -0
  13. data/exe/develoz +6 -0
  14. data/install +98 -0
  15. data/lib/develoz/canonical_fetcher.rb +56 -0
  16. data/lib/develoz/cli.rb +84 -0
  17. data/lib/develoz/generators/base.rb +134 -0
  18. data/lib/develoz/manifest.rb +44 -0
  19. data/lib/develoz/options.rb +55 -0
  20. data/lib/develoz/version.rb +5 -0
  21. data/lib/develoz/version_resolver.rb +103 -0
  22. data/lib/develoz.rb +15 -0
  23. data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
  24. data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
  25. data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
  26. data/lib/generators/develoz/admin/admin_generator.rb +44 -0
  27. data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
  28. data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
  29. data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
  30. data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
  31. data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
  32. data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
  33. data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
  34. data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
  35. data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
  36. data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
  37. data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
  38. data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
  39. data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
  40. data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
  41. data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
  42. data/lib/generators/develoz/api/api_generator.rb +55 -0
  43. data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
  44. data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
  45. data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
  46. data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
  47. data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
  48. data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
  49. data/lib/generators/develoz/auth/auth_generator.rb +82 -0
  50. data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
  51. data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
  52. data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
  53. data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
  54. data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
  55. data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
  56. data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
  57. data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
  58. data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
  59. data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
  60. data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
  61. data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
  62. data/lib/generators/develoz/ci/ci_generator.rb +66 -0
  63. data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
  64. data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
  65. data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
  66. data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
  67. data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
  68. data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
  69. data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
  70. data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
  71. data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
  72. data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
  73. data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
  74. data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
  75. data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
  76. data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
  77. data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
  78. data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
  79. data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
  80. data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
  81. data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
  82. data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
  83. data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
  84. data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
  85. data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
  86. data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
  87. data/lib/generators/develoz/database/database_generator.rb +44 -0
  88. data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
  89. data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
  90. data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
  91. data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
  92. data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
  93. data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
  94. data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
  95. data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
  96. data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
  97. data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
  98. data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
  99. data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
  100. data/lib/generators/develoz/docker/docker_generator.rb +54 -0
  101. data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
  102. data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
  103. data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
  104. data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
  105. data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
  106. data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
  107. data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
  108. data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
  109. data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
  110. data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
  111. data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
  112. data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
  113. data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
  114. data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
  115. data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
  116. data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
  117. data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
  118. data/lib/generators/develoz/install/install_generator.rb +51 -0
  119. data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
  120. data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
  121. data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
  122. data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
  123. data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
  124. data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
  125. data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
  126. data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
  127. data/lib/generators/develoz/push/push_generator.rb +61 -0
  128. data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
  129. data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
  130. data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
  131. data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
  132. data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
  133. data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
  134. data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
  135. data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
  136. data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
  137. data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
  138. data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
  139. data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
  140. data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
  141. data/lib/generators/develoz/solid/solid_generator.rb +52 -0
  142. data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
  143. data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
  144. data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
  145. data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
  146. data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
  147. data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
  148. data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
  149. data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
  150. data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
  151. data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
  152. data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
  153. data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
  154. data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
  155. data/lib/generators/develoz/testing/testing_generator.rb +39 -0
  156. data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
  157. data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
  158. data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
  159. data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
  160. data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
  161. data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
  162. data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
  163. data/lib/generators/develoz/ui/ui_generator.rb +57 -0
  164. data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
  165. data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
  166. data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
  167. data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
  168. data/lib/tasks/canonical.rake +27 -0
  169. data/templates/.keep +0 -0
  170. data/templates/CANONICAL_SOURCES.md +93 -0
  171. data/templates/pull_request_template.md.tt +13 -0
  172. metadata +383 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class AuthGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ def add_bcrypt_gem
13
+ add_gem "bcrypt"
14
+ end
15
+
16
+ def create_user_model
17
+ template "app/models/user.rb.tt", "app/models/user.rb"
18
+ end
19
+
20
+ def create_current_model
21
+ template "app/models/current.rb.tt", "app/models/current.rb"
22
+ end
23
+
24
+ def create_authentication_concern
25
+ template "app/controllers/concerns/authentication.rb.tt",
26
+ "app/controllers/concerns/authentication.rb"
27
+ end
28
+
29
+ def create_sessions_controller
30
+ template "app/controllers/sessions_controller.rb.tt",
31
+ "app/controllers/sessions_controller.rb"
32
+ end
33
+
34
+ def create_passwords_controller
35
+ template "app/controllers/passwords_controller.rb.tt",
36
+ "app/controllers/passwords_controller.rb"
37
+ end
38
+
39
+ def create_passwords_mailer
40
+ template "app/mailers/passwords_mailer.rb.tt",
41
+ "app/mailers/passwords_mailer.rb"
42
+ end
43
+
44
+ def create_sessions_views
45
+ template "app/views/sessions/new.html.erb.tt",
46
+ "app/views/sessions/new.html.erb"
47
+ end
48
+
49
+ def create_passwords_views
50
+ template "app/views/passwords/edit.html.erb.tt",
51
+ "app/views/passwords/edit.html.erb"
52
+ end
53
+
54
+ def create_passwords_mailer_views
55
+ template "app/views/passwords_mailer/reset.html.erb.tt",
56
+ "app/views/passwords_mailer/reset.html.erb"
57
+ end
58
+
59
+ def create_users_migration
60
+ return if migration_exists?("create_users")
61
+
62
+ template "db/migrate/create_users.rb.tt",
63
+ "db/migrate/#{next_migration_timestamp}_create_users.rb"
64
+ end
65
+
66
+ def insert_auth_routes
67
+ insert_route "resource :session, only: [:new, :create, :destroy]"
68
+ insert_route "resources :passwords, param: :token, only: [:new, :create, :edit, :update]"
69
+ end
70
+
71
+ def create_sessions_request_spec
72
+ template "spec/requests/sessions_spec.rb.tt",
73
+ "spec/requests/sessions_spec.rb"
74
+ end
75
+
76
+ def create_passwords_request_spec
77
+ template "spec/requests/passwords_spec.rb.tt",
78
+ "spec/requests/passwords_spec.rb"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :require_authentication
8
+ helper_method :authenticated?
9
+ end
10
+
11
+ class_methods do
12
+ def allow_authentication_as(**options)
13
+ skip_before_action :require_authentication, **options
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def authenticated?
20
+ resume_session
21
+ end
22
+
23
+ def require_authentication
24
+ resume_session || request_authentication
25
+ end
26
+
27
+ def resume_session
28
+ Current.user ||= find_user_by_cookie
29
+ end
30
+
31
+ def find_user_by_cookie
32
+ User.find_by(id: cookies.signed[:user_id]) if cookies.signed[:user_id]
33
+ end
34
+
35
+ def request_authentication
36
+ session[:return_to_after_authenticating] = request.url
37
+ redirect_to new_session_path
38
+ end
39
+
40
+ def after_authentication_url
41
+ session.delete(:return_to_after_authenticating) || root_url
42
+ end
43
+
44
+ def start_new_session_for(user)
45
+ Current.user = user
46
+ cookies.signed.permanent[:user_id] = { value: user.id, httponly: true, same_site: :lax }
47
+ end
48
+
49
+ def terminate_session
50
+ Current.user = nil
51
+ cookies.delete(:user_id)
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PasswordsController < ApplicationController
4
+ allow_authentication_as
5
+ before_action :set_user_by_token, only: %i[edit update]
6
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_password_path, alert: "Try again later." }
7
+
8
+ def new; end
9
+
10
+ def create
11
+ if (user = User.find_by(email: params[:email]))
12
+ PasswordsMailer.reset(user).deliver_later
13
+ end
14
+
15
+ redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
16
+ end
17
+
18
+ def edit; end
19
+
20
+ def update
21
+ if @user.update(params.permit(:password, :password_confirmation))
22
+ redirect_to new_session_path, notice: "Password has been reset."
23
+ else
24
+ redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def set_user_by_token
31
+ @user = User.find_by_password_reset_token!(params[:token])
32
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
33
+ redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SessionsController < ApplicationController
4
+ allow_authentication_as only: %i[new create]
5
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
6
+
7
+ def new; end
8
+
9
+ def create
10
+ if (user = User.authenticate_by(params.permit(:email, :password)))
11
+ start_new_session_for user
12
+ redirect_to after_authentication_url
13
+ else
14
+ redirect_to new_session_path, alert: "Try another email address or password."
15
+ end
16
+ end
17
+
18
+ def destroy
19
+ terminate_session
20
+ redirect_to new_session_path, status: :see_other
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PasswordsMailer < ApplicationMailer
4
+ def reset(user)
5
+ @user = user
6
+ mail subject: "Reset your password", to: user.email
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Current < ActiveSupport::CurrentAttributes
4
+ attribute :user
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ApplicationRecord
4
+ has_secure_password
5
+ normalizes :email, with: ->(e) { e.strip.downcase }
6
+ validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
7
+ end
@@ -0,0 +1,9 @@
1
+ <h1>Update your password</h1>
2
+
3
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4
+
5
+ <%%= form_with url: password_path(params[:token]), method: :put do |form| %>
6
+ <%%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
7
+ <%%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
8
+ <%%= form.submit "Save" %>
9
+ <%% end %>
@@ -0,0 +1,6 @@
1
+ <p>
2
+ You can reset your password on
3
+ <%%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
4
+
5
+ This link will expire in <%%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
6
+ </p>
@@ -0,0 +1,11 @@
1
+ <%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2
+ <%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3
+
4
+ <%%= form_with url: session_path do |form| %>
5
+ <%%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
6
+ <%%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
7
+ <%%= form.submit "Sign in" %>
8
+ <%% end %>
9
+ <br>
10
+
11
+ <%%= link_to "Forgot password?", new_password_path %>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateUsers < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :users do |t|
6
+ t.string :email, null: false
7
+ t.string :password_digest, null: false
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :users, :email, unique: true
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe "Passwords", type: :request do
6
+ describe "GET /password/new" do
7
+ it "renders the forgot password form" do
8
+ get new_password_path
9
+ expect(response).to have_http_status(:ok)
10
+ end
11
+ end
12
+
13
+ describe "POST /passwords" do
14
+ it "sends reset instructions when email exists" do
15
+ user = User.create!(email: "test@example.com", password: "password123")
16
+
17
+ expect { post passwords_path, params: { email: user.email } }
18
+ .to have_enqueued_mail(PasswordsMailer, :reset)
19
+ end
20
+
21
+ it "redirects to sign in with notice" do
22
+ post passwords_path, params: { email: "nonexistent@example.com" }
23
+ expect(response).to redirect_to(new_session_path)
24
+ expect(flash[:notice]).to be_present
25
+ end
26
+ end
27
+
28
+ describe "GET /password/:token/edit" do
29
+ it "renders the edit form for a valid token" do
30
+ user = User.create!(email: "test@example.com", password: "password123")
31
+
32
+ get edit_password_path(user.password_reset_token)
33
+ expect(response).to have_http_status(:ok)
34
+ end
35
+
36
+ it "redirects with alert for an invalid token" do
37
+ get edit_password_path("invalid-token")
38
+ expect(response).to redirect_to(new_password_path)
39
+ expect(flash[:alert]).to be_present
40
+ end
41
+ end
42
+
43
+ describe "PATCH /password/:token" do
44
+ it "resets the password with valid params" do
45
+ user = User.create!(email: "test@example.com", password: "password123")
46
+
47
+ patch password_path(user.password_reset_token), params: { password: "newpassword", password_confirmation: "newpassword" }
48
+ expect(response).to redirect_to(new_session_path)
49
+ expect(flash[:notice]).to be_present
50
+ end
51
+
52
+ it "redirects with alert when passwords do not match" do
53
+ user = User.create!(email: "test@example.com", password: "password123")
54
+
55
+ patch password_path(user.password_reset_token), params: { password: "newpassword", password_confirmation: "different" }
56
+ expect(response).to redirect_to(edit_password_path(user.password_reset_token))
57
+ expect(flash[:alert]).to be_present
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe "Sessions", type: :request do
6
+ describe "GET /session/new" do
7
+ it "renders the sign in form" do
8
+ get new_session_path
9
+ expect(response).to have_http_status(:ok)
10
+ end
11
+ end
12
+
13
+ describe "POST /session" do
14
+ it "signs in a valid user" do
15
+ user = User.create!(email: "test@example.com", password: "password123")
16
+
17
+ post session_path, params: { email: user.email, password: "password123" }
18
+ expect(response).to redirect_to(root_url)
19
+ end
20
+
21
+ it "redirects with alert for invalid credentials" do
22
+ post session_path, params: { email: "wrong@example.com", password: "wrong" }
23
+ expect(response).to redirect_to(new_session_path)
24
+ expect(flash[:alert]).to be_present
25
+ end
26
+ end
27
+
28
+ describe "DELETE /session" do
29
+ it "signs out the current user" do
30
+ user = User.create!(email: "test@example.com", password: "password123")
31
+ post session_path, params: { email: user.email, password: "password123" }
32
+
33
+ delete session_path
34
+ expect(response).to redirect_to(new_session_path)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class CiGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ def add_ci_gems
13
+ add_gem "rubocop-rails-omakase", group: %i[development test]
14
+ add_gem "reek", group: %i[development test]
15
+ add_gem "flay", group: %i[development test]
16
+ add_gem "brakeman", group: %i[development test]
17
+ add_gem "bundler-audit", group: %i[development test]
18
+ add_gem "haml_lint", require: false, group: %i[development test]
19
+ end
20
+
21
+ def create_ci_entrypoint
22
+ template "bin/ci.tt", "bin/ci"
23
+ end
24
+
25
+ def create_ci_config
26
+ template "config/ci.rb.tt", "config/ci.rb"
27
+ end
28
+
29
+ def create_rakefile
30
+ template "Rakefile.tt", "Rakefile"
31
+ end
32
+
33
+ def create_rubocop_config
34
+ template ".rubocop.yml.tt", ".rubocop.yml"
35
+ end
36
+
37
+ def create_reek_config
38
+ template ".reek.yml.tt", ".reek.yml"
39
+ end
40
+
41
+ def create_biome_config
42
+ template "biome.json.tt", "biome.json"
43
+ end
44
+
45
+ def create_stylelint_config
46
+ template ".stylelintrc.json.tt", ".stylelintrc.json"
47
+ end
48
+
49
+ def create_haml_lint_config
50
+ template ".haml-lint.yml.tt", ".haml-lint.yml"
51
+ end
52
+
53
+ def create_markdownlint_config
54
+ template ".markdownlint.json.tt", ".markdownlint.json"
55
+ end
56
+
57
+ def create_yamllint_config
58
+ template ".yamllint.tt", ".yamllint"
59
+ end
60
+
61
+ def create_ci_workflow
62
+ template ".github/workflows/ci.yml.tt", ".github/workflows/ci.yml"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ ci:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ bundler-cache: true
17
+ - name: Run CI pipeline
18
+ run: bin/ci
@@ -0,0 +1,12 @@
1
+ linters:
2
+ LineLength:
3
+ max: 120
4
+
5
+ ViewLength:
6
+ max: 120
7
+
8
+ InstanceVariables:
9
+ enabled: false
10
+
11
+ IdNames:
12
+ enabled: false
@@ -0,0 +1,10 @@
1
+ {
2
+ "default": true,
3
+ "MD013": {
4
+ "line_length": 120,
5
+ "code_blocks": false,
6
+ "tables": false
7
+ },
8
+ "MD033": false,
9
+ "MD041": false
10
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ exclude_paths:
3
+ - app/assets
4
+ - config
5
+ - db
6
+ - lib/tasks
7
+ - vendor
8
+ - spec
9
+ - node_modules
10
+
11
+ detectors:
12
+ IrresponsibleModule:
13
+ enabled: false
14
+ UtilityFunction:
15
+ enabled: false
16
+ ControlParameter:
17
+ enabled: false
18
+ BooleanParameter:
19
+ enabled: false
20
+ FeatureEnvy:
21
+ enabled: false
22
+ TooManyStatements:
23
+ max_statements: 10
24
+ TooManyMethods:
25
+ max_methods: 25
26
+ UncommunicativeVariableName:
27
+ accept:
28
+ - e
@@ -0,0 +1,22 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+ TargetRubyVersion: 3.4
7
+ Exclude:
8
+ - "bin/*"
9
+ - "db/schema.rb"
10
+ - "vendor/bundle/**/*"
11
+ - "node_modules/**/*"
12
+
13
+ Layout/LineLength:
14
+ Max: 120
15
+ AllowedPatterns:
16
+ - !ruby/regexp /^ *# /
17
+
18
+ Style/Documentation:
19
+ Enabled: false
20
+
21
+ Style/FrozenStringLiteralComment:
22
+ Enabled: false
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "stylelint-config-standard",
3
+ "ignoreFiles": ["app/assets/stylesheets/vendor/**/*"]
4
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ extends: default
3
+
4
+ ignore: |
5
+ node_modules/
6
+ vendor/
7
+ tmp/
8
+ public/
9
+
10
+ rules:
11
+ line-length: disable
12
+ document-start: disable
13
+ truthy:
14
+ check-keys: false
@@ -0,0 +1,73 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative 'config/application'
5
+
6
+ Rails.application.load_tasks
7
+
8
+ if Rails.env.development?
9
+ require 'rubocop/rake_task'
10
+ require 'haml_lint/rake_task'
11
+
12
+ namespace :lint do
13
+ RuboCop::RakeTask.new(:rubocop) do |task|
14
+ task.fail_on_error = true
15
+ task.options = %w[--parallel]
16
+ end
17
+
18
+ HamlLint::RakeTask.new(:haml)
19
+
20
+ desc 'Lint JavaScript/TypeScript with Biome'
21
+ task :biome do # rubocop:disable Rails/RakeEnvironment
22
+ sh 'yarn lint'
23
+ end
24
+
25
+ desc 'Lint CSS/SCSS with Stylelint'
26
+ task :stylelint do # rubocop:disable Rails/RakeEnvironment
27
+ sh 'yarn lint:css'
28
+ end
29
+
30
+ desc 'Lint Markdown with markdownlint'
31
+ task :markdownlint do # rubocop:disable Rails/RakeEnvironment
32
+ sh 'yarn lint:md'
33
+ end
34
+
35
+ desc 'Run Brakeman static security analysis'
36
+ task :brakeman do # rubocop:disable Rails/RakeEnvironment
37
+ sh 'bundle exec brakeman --quiet --no-pager --exit-on-warn'
38
+ end
39
+
40
+ desc 'Lint YAML with yamllint'
41
+ task :yamllint do # rubocop:disable Rails/RakeEnvironment
42
+ sh 'yamllint .'
43
+ end
44
+
45
+ desc 'Detect code smells with Reek'
46
+ task :reek do # rubocop:disable Rails/RakeEnvironment
47
+ sh 'bundle exec reek app lib'
48
+ end
49
+
50
+ desc 'Detect structural duplication with Flay'
51
+ task :flay do # rubocop:disable Rails/RakeEnvironment
52
+ # Mass 300 clears the existing by-design Filters:: DSL similarity (max ~272)
53
+ # while still catching new egregious duplication. Lower it as that debt is paid down.
54
+ output = `bundle exec flay --mass 300 app lib`
55
+ puts output
56
+ abort 'Flay detected structural duplication' unless output.include?('Total score (lower is better) = 0')
57
+ end
58
+ end
59
+
60
+ desc 'Run all linters (RuboCop, haml-lint, Biome, Stylelint, markdownlint, yamllint, Brakeman, Reek, Flay)'
61
+ task lint: %w[lint:rubocop lint:haml lint:biome lint:stylelint lint:markdownlint lint:yamllint
62
+ lint:brakeman lint:reek lint:flay]
63
+
64
+ namespace :docs do
65
+ desc 'Check platform-guide docs are in sync with their documented system specs'
66
+ task :check do # rubocop:disable Rails/RakeEnvironment
67
+ ruby 'bin/generate-docs --check'
68
+ end
69
+ end
70
+
71
+ Rake::Task[:default].clear if Rake::Task.task_defined?(:default)
72
+ task default: %w[lint spec:parallel docs:check]
73
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../config/boot"
3
+ require "active_support/continuous_integration"
4
+
5
+ CI = ActiveSupport::ContinuousIntegration
6
+ require_relative "../config/ci.rb"
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "includes": ["**", "!**/vendor/**", "!**/node_modules/**", "!public/**", "!**/coverage/**"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "useEditorconfig": true,
15
+ "formatWithErrors": false,
16
+ "indentStyle": "space",
17
+ "indentWidth": 2,
18
+ "lineEnding": "lf",
19
+ "lineWidth": 120
20
+ },
21
+ "linter": {
22
+ "enabled": true,
23
+ "rules": {
24
+ "recommended": true,
25
+ "correctness": {
26
+ "noUnusedVariables": "error"
27
+ },
28
+ "security": {
29
+ "noDangerouslySetInnerHtmlWithChildren": "error",
30
+ "noGlobalEval": "warn"
31
+ }
32
+ }
33
+ },
34
+ "javascript": {
35
+ "formatter": {
36
+ "quoteStyle": "single",
37
+ "semicolons": "asNeeded",
38
+ "trailingCommas": "es5"
39
+ }
40
+ }
41
+ }