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.
- checksums.yaml +4 -4
- data/app/controllers/supabase/rails/base_controller.rb +17 -0
- data/app/controllers/supabase/rails/oauth_controller.rb +53 -0
- data/app/controllers/supabase/rails/otp_controller.rb +55 -0
- data/app/controllers/supabase/rails/passwords_controller.rb +47 -0
- data/app/controllers/supabase/rails/registrations_controller.rb +46 -0
- data/app/controllers/supabase/rails/sessions_controller.rb +32 -0
- data/app/views/supabase/rails/oauth/_buttons.html.erb +8 -0
- data/app/views/supabase/rails/otp/new.html.erb +11 -0
- data/app/views/supabase/rails/otp/verify.html.erb +14 -0
- data/app/views/supabase/rails/passwords/edit.html.erb +9 -0
- data/app/views/supabase/rails/passwords/new.html.erb +11 -0
- data/app/views/supabase/rails/registrations/new.html.erb +12 -0
- data/app/views/supabase/rails/sessions/new.html.erb +15 -0
- data/app/views/supabase/rails/shared/_flash.html.erb +5 -0
- data/config/locales/en.yml +19 -0
- data/lib/generators/supabase/install/install_generator.rb +84 -0
- data/lib/generators/supabase/install/templates/app/controllers/concerns/authentication.rb.tt +9 -0
- data/lib/generators/supabase/install/templates/app/controllers/oauth_controller.rb.tt +4 -0
- data/lib/generators/supabase/install/templates/app/controllers/otp_controller.rb.tt +4 -0
- data/lib/generators/supabase/install/templates/app/controllers/passwords_controller.rb.tt +4 -0
- data/lib/generators/supabase/install/templates/app/controllers/registrations_controller.rb.tt +4 -0
- data/lib/generators/supabase/install/templates/app/controllers/sessions_controller.rb.tt +4 -0
- data/lib/generators/supabase/install/templates/app/models/current.rb.tt +5 -0
- data/lib/generators/supabase/install/templates/config/initializers/supabase.rb.tt +21 -0
- data/lib/generators/supabase/user_model/templates/app/models/user.rb.tt +11 -0
- data/lib/generators/supabase/user_model/templates/db/migrate/create_supabase_users.rb.tt +11 -0
- data/lib/generators/supabase/user_model/user_model_generator.rb +64 -0
- data/lib/generators/supabase/views/views_generator.rb +33 -0
- data/lib/supabase/rails/authentication.rb +744 -0
- data/lib/supabase/rails/context.rb +22 -5
- data/lib/supabase/rails/controller.rb +24 -2
- data/lib/supabase/rails/core.rb +9 -0
- data/lib/supabase/rails/engine.rb +35 -0
- data/lib/supabase/rails/errors.rb +68 -0
- data/lib/supabase/rails/middleware.rb +30 -7
- data/lib/supabase/rails/railtie.rb +28 -2
- data/lib/supabase/rails/routes.rb +89 -0
- data/lib/supabase/rails/session_store.rb +127 -0
- data/lib/supabase/rails/user.rb +38 -0
- data/lib/supabase/rails/version.rb +1 -1
- data/lib/supabase/rails/web/auth_client_factory.rb +101 -0
- data/lib/supabase/rails/web/auth_error_mapper.rb +65 -0
- data/lib/supabase/rails/web/cookie_credential_strategy.rb +200 -0
- data/lib/supabase/rails/web/redirect_validator.rb +92 -0
- data/lib/supabase/rails/web/refresh_coordinator.rb +78 -0
- data/lib/supabase/rails/web/request_scoped_storage.rb +132 -0
- data/lib/supabase/rails.rb +10 -0
- metadata +54 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a290d81c0aa9acd877b7aeb8a1c94106a16641d7b54538c3b0c0f3551650967c
|
|
4
|
+
data.tar.gz: cc40eee184c781beebeab688ac3d78b788b327bb01397572d9ff36b332c88a3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,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,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
|