mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 23:45:21 +02:00
Fix OAuth mobile app support with custom URL schemes
- Configure Doorkeeper to allow custom URL schemes (maybeapp://)
- Disable force_ssl_in_redirect_uri to support non-HTTPS schemes
- Add custom Doorkeeper views with mobile OAuth detection
- Disable Turbo for mobile OAuth flows to prevent redirect interference
- Add display parameter preservation through OAuth flow
- Create custom Doorkeeper layouts with proper styling
- Add comprehensive integration tests for mobile OAuth flows
- Ensure all OAuth pages use proper doorkeeper/application layout
This allows the mobile app to complete OAuth authorization flows
without the web app interfering with custom URL scheme redirects.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
404066eaa1
commit
cba0bdf0e2
17 changed files with 513 additions and 4 deletions
22
app/views/doorkeeper/authorizations/error.html.erb
Normal file
22
app/views/doorkeeper/authorizations/error.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-destructive-surface flex items-center justify-center mb-4">
|
||||
<%= icon("alert-circle", class: "w-6 h-6 text-destructive") %>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-primary"><%= t('doorkeeper.authorizations.error.title') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-inset rounded-lg p-4">
|
||||
<p class="text-sm text-secondary">
|
||||
<%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<%= render LinkComponent.new(
|
||||
text: "Go back",
|
||||
href: "javascript:history.back()",
|
||||
variant: :secondary
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
22
app/views/doorkeeper/authorizations/form_post.html.erb
Normal file
22
app/views/doorkeeper/authorizations/form_post.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-surface-inset flex items-center justify-center mb-4">
|
||||
<%= icon("loader-circle", class: "w-6 h-6 text-primary animate-spin") %>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-primary"><%= t('.title') %></h1>
|
||||
<p class="text-sm text-secondary">Redirecting you back to the application...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% turbo_disabled = @pre_auth.redirect_uri&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
||||
<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false, data: { turbo: !turbo_disabled } do %>
|
||||
<% auth.body.compact.each do |key, value| %>
|
||||
<%= hidden_field_tag key, value %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
window.onload = function () {
|
||||
document.forms['redirect_form'].submit();
|
||||
};
|
||||
</script>
|
76
app/views/doorkeeper/authorizations/new.html.erb
Normal file
76
app/views/doorkeeper/authorizations/new.html.erb
Normal file
|
@ -0,0 +1,76 @@
|
|||
<% if params[:redirect_uri]&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
||||
<meta name="turbo-visit-control" content="reload">
|
||||
<% end %>
|
||||
|
||||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<p class="text-sm text-secondary">
|
||||
<%= raw t('.prompt', client_name: content_tag(:span, @pre_auth.client.name, class: 'font-medium text-primary')) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if @pre_auth.scopes.count > 0 %>
|
||||
<div class="bg-surface-inset rounded-lg p-4 space-y-3">
|
||||
<p class="text-sm font-medium text-primary"><%= t('.able_to') %>:</p>
|
||||
<ul class="space-y-2">
|
||||
<% @pre_auth.scopes.each do |scope| %>
|
||||
<li class="flex items-start gap-2 text-sm text-secondary">
|
||||
<%= icon("check", class: "w-4 h-4 mt-0.5 text-success") %>
|
||||
<span><%= t scope, scope: [:doorkeeper, :scopes] %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-3">
|
||||
<% turbo_disabled = params[:redirect_uri]&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
||||
<%= form_tag oauth_authorization_path, method: :post, class: "w-full", data: { turbo: !turbo_disabled } do %>
|
||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
|
||||
<%= hidden_field_tag :state, @pre_auth.state, id: nil %>
|
||||
<%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %>
|
||||
<%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %>
|
||||
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
||||
<% if params[:display].present? %>
|
||||
<%= hidden_field_tag :display, params[:display], id: nil %>
|
||||
<% end %>
|
||||
<%= render ButtonComponent.new(
|
||||
text: t('doorkeeper.authorizations.buttons.authorize'),
|
||||
variant: :primary,
|
||||
size: :lg,
|
||||
full_width: true,
|
||||
href: oauth_authorization_path,
|
||||
data: { disable_with: "Authorizing..." }
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= form_tag oauth_authorization_path, method: :delete, class: "w-full", data: { turbo: !turbo_disabled } do %>
|
||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
|
||||
<%= hidden_field_tag :state, @pre_auth.state, id: nil %>
|
||||
<%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %>
|
||||
<%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %>
|
||||
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
||||
<% if params[:display].present? %>
|
||||
<%= hidden_field_tag :display, params[:display], id: nil %>
|
||||
<% end %>
|
||||
<%= render ButtonComponent.new(
|
||||
text: t('doorkeeper.authorizations.buttons.deny'),
|
||||
variant: :outline,
|
||||
size: :lg,
|
||||
full_width: true,
|
||||
href: oauth_authorization_path,
|
||||
data: { disable_with: "Denying..." }
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-tertiary text-center">
|
||||
By authorizing, you allow this app to access your Maybe data according to the permissions above.
|
||||
</p>
|
||||
</div>
|
17
app/views/doorkeeper/authorizations/show.html.erb
Normal file
17
app/views/doorkeeper/authorizations/show.html.erb
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-success-surface flex items-center justify-center mb-4">
|
||||
<%= icon("check", class: "w-6 h-6 text-success") %>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-primary"><%= t('.title') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-inset rounded-lg p-4">
|
||||
<p class="text-xs text-secondary mb-2">Authorization Code:</p>
|
||||
<code id="authorization_code" class="block text-sm font-mono text-primary break-all"><%= params[:code] %></code>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-secondary text-center">
|
||||
Copy this code and paste it into the application.
|
||||
</p>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue