supabase-rails 0.1.0 → 1.0.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/supabase/rails/base_controller.rb +17 -0
  3. data/app/controllers/supabase/rails/oauth_controller.rb +53 -0
  4. data/app/controllers/supabase/rails/otp_controller.rb +55 -0
  5. data/app/controllers/supabase/rails/passwords_controller.rb +47 -0
  6. data/app/controllers/supabase/rails/registrations_controller.rb +46 -0
  7. data/app/controllers/supabase/rails/sessions_controller.rb +32 -0
  8. data/app/views/supabase/rails/oauth/_buttons.html.erb +8 -0
  9. data/app/views/supabase/rails/otp/new.html.erb +11 -0
  10. data/app/views/supabase/rails/otp/verify.html.erb +14 -0
  11. data/app/views/supabase/rails/passwords/edit.html.erb +9 -0
  12. data/app/views/supabase/rails/passwords/new.html.erb +11 -0
  13. data/app/views/supabase/rails/registrations/new.html.erb +12 -0
  14. data/app/views/supabase/rails/sessions/new.html.erb +15 -0
  15. data/app/views/supabase/rails/shared/_flash.html.erb +5 -0
  16. data/config/locales/en.yml +19 -0
  17. data/lib/generators/supabase/install/install_generator.rb +84 -0
  18. data/lib/generators/supabase/install/templates/app/controllers/concerns/authentication.rb.tt +9 -0
  19. data/lib/generators/supabase/install/templates/app/controllers/oauth_controller.rb.tt +4 -0
  20. data/lib/generators/supabase/install/templates/app/controllers/otp_controller.rb.tt +4 -0
  21. data/lib/generators/supabase/install/templates/app/controllers/passwords_controller.rb.tt +4 -0
  22. data/lib/generators/supabase/install/templates/app/controllers/registrations_controller.rb.tt +4 -0
  23. data/lib/generators/supabase/install/templates/app/controllers/sessions_controller.rb.tt +4 -0
  24. data/lib/generators/supabase/install/templates/app/models/current.rb.tt +5 -0
  25. data/lib/generators/supabase/install/templates/config/initializers/supabase.rb.tt +21 -0
  26. data/lib/generators/supabase/user_model/templates/app/models/user.rb.tt +11 -0
  27. data/lib/generators/supabase/user_model/templates/db/migrate/create_supabase_users.rb.tt +11 -0
  28. data/lib/generators/supabase/user_model/user_model_generator.rb +64 -0
  29. data/lib/generators/supabase/views/views_generator.rb +33 -0
  30. data/lib/supabase/rails/authentication.rb +744 -0
  31. data/lib/supabase/rails/context.rb +22 -5
  32. data/lib/supabase/rails/controller.rb +24 -2
  33. data/lib/supabase/rails/core.rb +9 -0
  34. data/lib/supabase/rails/engine.rb +35 -0
  35. data/lib/supabase/rails/errors.rb +68 -0
  36. data/lib/supabase/rails/middleware.rb +30 -7
  37. data/lib/supabase/rails/railtie.rb +28 -2
  38. data/lib/supabase/rails/routes.rb +89 -0
  39. data/lib/supabase/rails/session_store.rb +127 -0
  40. data/lib/supabase/rails/user.rb +38 -0
  41. data/lib/supabase/rails/version.rb +1 -1
  42. data/lib/supabase/rails/web/auth_client_factory.rb +101 -0
  43. data/lib/supabase/rails/web/auth_error_mapper.rb +65 -0
  44. data/lib/supabase/rails/web/cookie_credential_strategy.rb +200 -0
  45. data/lib/supabase/rails/web/redirect_validator.rb +92 -0
  46. data/lib/supabase/rails/web/refresh_coordinator.rb +78 -0
  47. data/lib/supabase/rails/web/request_scoped_storage.rb +132 -0
  48. data/lib/supabase/rails.rb +10 -0
  49. metadata +54 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecccb50074bb2287f87d4da6d42dd824d0857351c778c91ac6a13e4ad09d7258
4
- data.tar.gz: 8e2e06f0425dcdedb6fdcedc19db9f44d731d09eb90709b136716dd819217e2e
3
+ metadata.gz: a290d81c0aa9acd877b7aeb8a1c94106a16641d7b54538c3b0c0f3551650967c
4
+ data.tar.gz: cc40eee184c781beebeab688ac3d78b788b327bb01397572d9ff36b332c88a3f
5
5
  SHA512:
6
- metadata.gz: b08cac9795f94d01611d95f8b3115f6978b151054b0879ae865e5bf888de93bf21b8f15b3a811d0b985bea95e398a7d4788c0439bbce8f1b8d53b36e8f292ce2
7
- data.tar.gz: 93b55c0c766f4f7a145769861b8818e182f93b5856f568f2c6f00f143ffc6b036de191fc7cb6a7e24827b7a83d76e86069f0c00131ab7cc9c438976cc9dd865f
6
+ metadata.gz: 48c4e01998a6a31e563e34623c187e4d9aaa2b219190c5c96bd541a503027c9b3da61f30b1598802a891de3f1cf23dbbeda4ca90788261e1e4684c44c1e6def8
7
+ data.tar.gz: b9663dad4a063e83d0b4111136197d805b2c37965d5b193ca98ea87246fa511accdfc6ad131c73039d7bed72527965d60ebf704ec78a7649682d4fdc69455059
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Rails
5
+ # Base class for all `Supabase::Rails::*Controller` shells (FR-W8 / US-017).
6
+ # Inherits from the host app's `::ApplicationController` so layouts, CSRF
7
+ # protection, helpers, and any global before_actions defined on the host's
8
+ # base class flow through to the gem's controllers. Includes the
9
+ # {Supabase::Rails::Authentication} concern so the FR-W5 surface
10
+ # (`require_authentication` before_action, `allow_unauthenticated_access`
11
+ # macro, `start_new_session_for`, `terminate_session`, low-level
12
+ # `supabase_*` helpers, override hooks) is available to subclasses.
13
+ class BaseController < ::ApplicationController
14
+ include Supabase::Rails::Authentication
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Rails
5
+ # OAuth 2.0 + PKCE (FR-W7 / US-014):
6
+ #
7
+ # * `authorize` — `GET /oauth/:provider`. Generates the PKCE state +
8
+ # verifier (stashed in a signed cookie per `RequestScopedStorage`),
9
+ # then redirects the user to the upstream provider's authorize URL
10
+ # via Supabase Auth.
11
+ # * `callback` — `GET /oauth/callback`. Exchanges the `code` + `state`
12
+ # for a session via
13
+ # {Supabase::Rails::Authentication#supabase_exchange_code_for_session}.
14
+ # A missing PKCE verifier (cookie expired or state never issued)
15
+ # fast-fails to a flash message + back to sign-in without bothering
16
+ # the upstream.
17
+ #
18
+ # `redirect_to:` on `authorize` is validated against
19
+ # `config.supabase.allowed_redirect_origins` (FR-W11) before the upstream
20
+ # call, so an attacker can't smuggle in an off-allowlist destination.
21
+ class OauthController < BaseController
22
+ allow_unauthenticated_access only: %i[authorize callback]
23
+
24
+ def authorize
25
+ result = supabase_sign_in_with_oauth(
26
+ provider: params[:provider],
27
+ redirect_to: params[:redirect_to] || oauth_callback_url
28
+ )
29
+
30
+ if result.success?
31
+ redirect_to result.value, allow_other_host: true
32
+ else
33
+ redirect_to new_session_path,
34
+ alert: I18n.t("supabase.rails.oauth.failed")
35
+ end
36
+ end
37
+
38
+ def callback
39
+ result = supabase_exchange_code_for_session(
40
+ code: params[:code],
41
+ state: params[:state]
42
+ )
43
+
44
+ if result.success?
45
+ redirect_to after_authentication_url,
46
+ notice: I18n.t("supabase.rails.oauth.connected")
47
+ else
48
+ redirect_to new_session_path, alert: result.error.message
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Rails
5
+ # Passwordless sign-in via OTP / magic link (FR-W7 / US-013):
6
+ #
7
+ # * `new` / `create` — user supplies an email or phone; gem calls
8
+ # {Supabase::Rails::Authentication#supabase_sign_in_with_otp} which
9
+ # triggers delivery (no session yet). Routes to `verify`.
10
+ # * `verify` — accepts both GET (render the code-entry form) and POST
11
+ # (submit the code via
12
+ # {Supabase::Rails::Authentication#supabase_verify_otp}). On success
13
+ # the helper writes the session cookie and we redirect to
14
+ # `after_authentication_url`.
15
+ class OtpController < BaseController
16
+ allow_unauthenticated_access only: %i[new create verify]
17
+
18
+ def new; end
19
+
20
+ def create
21
+ result = supabase_sign_in_with_otp(
22
+ email: params[:email],
23
+ phone: params[:phone]
24
+ )
25
+
26
+ if result.success?
27
+ redirect_to verify_otp_index_path,
28
+ notice: I18n.t("supabase.rails.otp.sent")
29
+ else
30
+ flash.now[:alert] = result.error.message
31
+ render :new, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def verify
36
+ return unless request.post?
37
+
38
+ result = supabase_verify_otp(
39
+ token: params[:token],
40
+ type: params[:type] || "email",
41
+ email: params[:email],
42
+ phone: params[:phone]
43
+ )
44
+
45
+ if result.success?
46
+ redirect_to after_authentication_url,
47
+ notice: I18n.t("supabase.rails.otp.verified")
48
+ else
49
+ flash.now[:alert] = result.error.message
50
+ render :verify, status: :unprocessable_entity
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Rails
5
+ # Password reset (two-step flow):
6
+ #
7
+ # * `new` / `create` — user requests a reset email. Supabase Auth
8
+ # handles the email content + tokenized link.
9
+ # * `edit` / `update` — user lands here after clicking the recovery
10
+ # link. The recovery deep-link delivers them with an authenticated
11
+ # session in the encrypted cookie, so {#update} can call
12
+ # {Supabase::Rails::Authentication#supabase_update_user} to set the
13
+ # new password (which goes through the session-seeded auth client
14
+ # per FR-W6 / US-016).
15
+ class PasswordsController < BaseController
16
+ allow_unauthenticated_access only: %i[new create edit update]
17
+
18
+ def new; end
19
+
20
+ def create
21
+ result = supabase_reset_password(email: params[:email])
22
+
23
+ if result.success?
24
+ redirect_to new_session_path,
25
+ notice: I18n.t("supabase.rails.passwords.reset_sent")
26
+ else
27
+ flash.now[:alert] = result.error.message
28
+ render :new, status: :unprocessable_entity
29
+ end
30
+ end
31
+
32
+ def edit; end
33
+
34
+ def update
35
+ result = supabase_update_user(password: params[:password])
36
+
37
+ if result.success?
38
+ redirect_to new_session_path,
39
+ notice: I18n.t("supabase.rails.passwords.updated")
40
+ else
41
+ flash.now[:alert] = result.error.message
42
+ render :edit, status: :unprocessable_entity
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Rails
5
+ # Email + password sign-up. Two-branch happy path:
6
+ #
7
+ # * `result.value.session` is non-nil → auto-sign-in is enabled on the
8
+ # Supabase project → user is signed in immediately.
9
+ # * `result.value.session` is nil → email-confirmation is required →
10
+ # render a "Check your inbox" notice and route back to the sign-in
11
+ # page so the user lands there after clicking the confirmation link.
12
+ #
13
+ # On a 4xx upstream failure {Supabase::Rails::Authentication#supabase_sign_up}
14
+ # surfaces the mapped {AuthError} so the host can show specific UI for
15
+ # `WEAK_PASSWORD` (422) while masking other 4xx errors to a generic
16
+ # "Invalid credentials" (FR-W7 / US-012). 5xx flashes a generic message.
17
+ class RegistrationsController < BaseController
18
+ allow_unauthenticated_access only: %i[new create]
19
+
20
+ def new; end
21
+
22
+ def create
23
+ result = supabase_sign_up(email: params[:email], password: params[:password])
24
+
25
+ if result.success?
26
+ if registered_session_present?(result.value)
27
+ redirect_to after_authentication_url,
28
+ notice: I18n.t("supabase.rails.registrations.created")
29
+ else
30
+ redirect_to new_session_path,
31
+ notice: I18n.t("supabase.rails.registrations.pending_confirmation")
32
+ end
33
+ else
34
+ flash.now[:alert] = result.error.message
35
+ render :new, status: :unprocessable_entity
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def registered_session_present?(value)
42
+ value.respond_to?(:session) && !value.session.nil?
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Rails
5
+ # Email + password sign-in / sign-out. Subclassed in the host app by
6
+ # the install generator (FR-W12) so updates to flash copy, error
7
+ # handling, redirect destinations, etc. ship via `bundle update`.
8
+ # Hosts override any action by redefining it in the subclass.
9
+ class SessionsController < BaseController
10
+ allow_unauthenticated_access only: %i[new create]
11
+
12
+ def new; end
13
+
14
+ def create
15
+ if (supabase_session = authenticate_with_supabase(email: params[:email], password: params[:password]))
16
+ start_new_session_for(supabase_session)
17
+ redirect_to after_authentication_url,
18
+ notice: I18n.t("supabase.rails.sessions.created")
19
+ else
20
+ flash.now[:alert] = I18n.t("supabase.rails.sessions.invalid")
21
+ render :new, status: :unauthorized
22
+ end
23
+ end
24
+
25
+ def destroy
26
+ terminate_session
27
+ redirect_to root_path,
28
+ notice: I18n.t("supabase.rails.sessions.destroyed")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ <%# OAuth provider buttons. Renders one link per provider listed in
2
+ `config.supabase.oauth_providers` (defaults to none — host sets this in
3
+ the supabase initializer). Each link initiates the PKCE flow via
4
+ `OauthController#authorize`. %>
5
+ <% providers = (Rails.application.config.supabase.oauth_providers if defined?(::Rails)) %>
6
+ <% Array(providers).each do |provider| %>
7
+ <%= link_to "Sign in with #{provider.to_s.capitalize}", oauth_authorize_path(provider: provider), data: { turbo_method: :get } %><br>
8
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <h1>Sign in with a one-time code</h1>
2
+
3
+ <%= render "supabase/rails/shared/flash" %>
4
+
5
+ <%= form_with url: otp_index_path do |form| %>
6
+ <%= form.email_field :email, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
7
+ <%= form.submit "Send me a code" %>
8
+ <% end %>
9
+ <br>
10
+
11
+ <%= link_to "Back to sign in", new_session_path %>
@@ -0,0 +1,14 @@
1
+ <h1>Enter your code</h1>
2
+
3
+ <%= render "supabase/rails/shared/flash" %>
4
+
5
+ <%= form_with url: verify_otp_index_path do |form| %>
6
+ <%= form.hidden_field :email, value: params[:email] %>
7
+ <%= form.hidden_field :phone, value: params[:phone] %>
8
+ <%= form.hidden_field :type, value: params[:type] || "email" %>
9
+ <%= form.text_field :token, required: true, autofocus: true, autocomplete: "one-time-code", inputmode: "numeric", placeholder: "Enter the code we just sent" %><br>
10
+ <%= form.submit "Verify" %>
11
+ <% end %>
12
+ <br>
13
+
14
+ <%= link_to "Back to sign in", new_session_path %>
@@ -0,0 +1,9 @@
1
+ <h1>Update your password</h1>
2
+
3
+ <%= render "supabase/rails/shared/flash" %>
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,11 @@
1
+ <h1>Forgot your password?</h1>
2
+
3
+ <%= render "supabase/rails/shared/flash" %>
4
+
5
+ <%= form_with url: passwords_path do |form| %>
6
+ <%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
7
+ <%= form.submit "Email reset instructions" %>
8
+ <% end %>
9
+ <br>
10
+
11
+ <%= link_to "Back to sign in", new_session_path %>
@@ -0,0 +1,12 @@
1
+ <h1>Create an account</h1>
2
+
3
+ <%= render "supabase/rails/shared/flash" %>
4
+
5
+ <%= form_with url: registration_path do |form| %>
6
+ <%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
7
+ <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Choose a password", maxlength: 72 %><br>
8
+ <%= form.submit "Sign up" %>
9
+ <% end %>
10
+ <br>
11
+
12
+ <%= link_to "Already have an account? Sign in", new_session_path %>
@@ -0,0 +1,15 @@
1
+ <h1>Sign in</h1>
2
+
3
+ <%= render "supabase/rails/shared/flash" %>
4
+
5
+ <%= form_with url: session_path do |form| %>
6
+ <%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
7
+ <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
8
+ <%= form.submit "Sign in" %>
9
+ <% end %>
10
+ <br>
11
+
12
+ <%= link_to "Forgot password?", new_password_path %><br>
13
+ <%= link_to "Create an account", new_registration_path %>
14
+
15
+ <%= render "supabase/rails/oauth/buttons" %>
@@ -0,0 +1,5 @@
1
+ <%# Flash partial — shared by every gem-shipped view. Uses the conventional
2
+ `notice` / `alert` keys (AC-4). Hosts can override this partial by shipping
3
+ their own at `app/views/supabase/rails/shared/_flash.html.erb`. %>
4
+ <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
5
+ <%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
@@ -0,0 +1,19 @@
1
+ en:
2
+ supabase:
3
+ rails:
4
+ sessions:
5
+ created: "Signed in successfully."
6
+ invalid: "Try another email address or password."
7
+ destroyed: "Signed out successfully."
8
+ registrations:
9
+ created: "Welcome — your account is ready."
10
+ pending_confirmation: "Check your inbox to confirm your email before signing in."
11
+ passwords:
12
+ reset_sent: "Check your inbox for a password-reset link."
13
+ updated: "Password updated. Sign in with your new password."
14
+ otp:
15
+ sent: "We sent you a code — enter it below."
16
+ verified: "Signed in successfully."
17
+ oauth:
18
+ failed: "We couldn't start that sign-in. Please try again."
19
+ connected: "Signed in successfully."
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Supabase
6
+ module Generators
7
+ # `rails g supabase:install` (FR-W12 / US-018).
8
+ #
9
+ # Emits the host-app glue for `:web` mode: an `Authentication` concern
10
+ # that includes the gem's module, five near-empty controllers that
11
+ # subclass the gem's `Supabase::Rails::*Controller` base classes, a
12
+ # `Current` model, and a config initializer. Also patches
13
+ # `config/routes.rb` (adds `supabase_authentication_routes`) and
14
+ # `app/controllers/application_controller.rb` (adds `include Authentication`).
15
+ #
16
+ # Idempotent — Thor's `template` + `inject_into_file` skip writes when
17
+ # the target already matches the desired content, and prompt the user
18
+ # to overwrite when files diverge from the gem's defaults.
19
+ class InstallGenerator < ::Rails::Generators::Base
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ desc <<~DESC
23
+ Installs Supabase-backed authentication into the host app.
24
+
25
+ Emits an Authentication concern, controllers, a Current model, and
26
+ a config initializer, then patches config/routes.rb and
27
+ application_controller.rb to wire them up.
28
+ DESC
29
+
30
+ def create_authentication_concern
31
+ template "app/controllers/concerns/authentication.rb.tt",
32
+ "app/controllers/concerns/authentication.rb"
33
+ end
34
+
35
+ def create_controllers
36
+ %w[sessions registrations passwords otp oauth].each do |name|
37
+ template "app/controllers/#{name}_controller.rb.tt",
38
+ "app/controllers/#{name}_controller.rb"
39
+ end
40
+ end
41
+
42
+ def create_current_model
43
+ template "app/models/current.rb.tt", "app/models/current.rb"
44
+ end
45
+
46
+ def create_initializer
47
+ template "config/initializers/supabase.rb.tt",
48
+ "config/initializers/supabase.rb"
49
+ end
50
+
51
+ def patch_routes
52
+ routes_path = "config/routes.rb"
53
+ return unless File.exist?(File.expand_path(routes_path, destination_root))
54
+ return if routes_already_patched?(routes_path)
55
+
56
+ inject_into_file routes_path,
57
+ " supabase_authentication_routes\n",
58
+ after: "Rails.application.routes.draw do\n"
59
+ end
60
+
61
+ def patch_application_controller
62
+ app_controller_path = "app/controllers/application_controller.rb"
63
+ return unless File.exist?(File.expand_path(app_controller_path, destination_root))
64
+ return if application_controller_already_patched?(app_controller_path)
65
+
66
+ inject_into_class app_controller_path, "ApplicationController" do
67
+ " include Authentication\n"
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def routes_already_patched?(routes_path)
74
+ absolute = File.expand_path(routes_path, destination_root)
75
+ File.read(absolute).include?("supabase_authentication_routes")
76
+ end
77
+
78
+ def application_controller_already_patched?(app_controller_path)
79
+ absolute = File.expand_path(app_controller_path, destination_root)
80
+ File.read(absolute).include?("include Authentication")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authentication
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Supabase::Rails::Authentication
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OauthController < Supabase::Rails::OauthController
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OtpController < Supabase::Rails::OtpController
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PasswordsController < Supabase::Rails::PasswordsController
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RegistrationsController < Supabase::Rails::RegistrationsController
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SessionsController < Supabase::Rails::SessionsController
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Current < ActiveSupport::CurrentAttributes
4
+ attribute :user, :session
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.supabase.mode = :web
4
+
5
+ # Origins the OAuth + password-reset helpers will accept as redirect targets.
6
+ # Path-only redirects are always allowed; absolute URLs must match an entry
7
+ # below. Defaults to [request.host] at runtime when this list is empty.
8
+ # Rails.application.config.supabase.allowed_redirect_origins = ["https://example.com"]
9
+
10
+ # Expose `current_user` as a view helper. nil = derive from mode
11
+ # (true in :web, false in :api).
12
+ # Rails.application.config.supabase.expose_current_user = nil
13
+
14
+ # Encrypted session cookie defaults. `secure: nil` = auto-detect from Rails.env.
15
+ # Rails.application.config.supabase.session = {
16
+ # cookie_name: "sb-session",
17
+ # same_site: :lax,
18
+ # secure: nil,
19
+ # domain: nil,
20
+ # path: "/"
21
+ # }
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ApplicationRecord
4
+ self.primary_key = :id
5
+
6
+ def self.from_supabase(claims)
7
+ find_or_create_by!(id: claims["sub"]) do |u|
8
+ u.email = claims["email"]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSupabaseUsers < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :users, id: :uuid do |t|
6
+ t.string :email
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "rails/generators/migration"
5
+ require "active_support/core_ext/string/strip"
6
+
7
+ module Supabase
8
+ module Generators
9
+ # `rails g supabase:user_model` (FR-W14 / US-025).
10
+ #
11
+ # Opt-in shadow `User` ActiveRecord model. Emits a `users` migration
12
+ # with a UUID primary key matching Supabase's `user.id`, an AR model
13
+ # with a `from_supabase(claims)` upsert helper, and patches the
14
+ # `config/initializers/supabase.rb` to set
15
+ # `config.supabase.user_model = "User"` so the Authentication concern
16
+ # (US-016) substitutes the AR record for `Current.user`.
17
+ class UserModelGenerator < ::Rails::Generators::Base
18
+ include ::Rails::Generators::Migration
19
+
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ desc <<~DESC
23
+ Installs an opt-in shadow `User` AR model backed by a UUID-keyed
24
+ `users` table that mirrors Supabase auth.users.
25
+
26
+ Emits a migration, an `app/models/user.rb` with a `from_supabase`
27
+ upsert, and patches `config/initializers/supabase.rb` to set
28
+ `config.supabase.user_model = "User"`.
29
+ DESC
30
+
31
+ # Rails' built-in timestamp for migration filenames. Mirrors what
32
+ # `ActiveRecord::Generators::Migration#next_migration_number` does
33
+ # so the generator works standalone (without ActiveRecord loaded).
34
+ def self.next_migration_number(_dirname)
35
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
36
+ end
37
+
38
+ def create_migration_file
39
+ migration_template "db/migrate/create_supabase_users.rb.tt",
40
+ "db/migrate/create_supabase_users.rb"
41
+ end
42
+
43
+ def create_user_model
44
+ template "app/models/user.rb.tt", "app/models/user.rb"
45
+ end
46
+
47
+ def patch_initializer
48
+ initializer_path = "config/initializers/supabase.rb"
49
+ return unless File.exist?(File.expand_path(initializer_path, destination_root))
50
+ return if initializer_already_patched?(initializer_path)
51
+
52
+ append_to_file initializer_path,
53
+ %(\nRails.application.config.supabase.user_model = "User"\n)
54
+ end
55
+
56
+ private
57
+
58
+ def initializer_already_patched?(initializer_path)
59
+ absolute = File.expand_path(initializer_path, destination_root)
60
+ File.read(absolute).match?(/^\s*[^#\n]*config\.supabase\.user_model\s*=/)
61
+ end
62
+ end
63
+ end
64
+ end