diff --git a/Gemfile.lock b/Gemfile.lock index 7d7c15c..d00f0be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,7 +88,7 @@ GEM activerecord (>= 6.0.0) activesupport (>= 6.0.0) ast (2.4.3) - avo (3.24.0) + avo (3.24.1) actionview (>= 6.1) active_link_to activerecord (>= 6.1) @@ -346,19 +346,19 @@ GEM net-protocol net-ssh (7.3.0) nio4r (2.7.4) - nokogiri (1.18.9-aarch64-linux-gnu) + nokogiri (1.18.10-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-aarch64-linux-musl) + nokogiri (1.18.10-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.9-arm-linux-gnu) + nokogiri (1.18.10-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-arm-linux-musl) + nokogiri (1.18.10-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.9-arm64-darwin) + nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-gnu) + nokogiri (1.18.10-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-musl) + nokogiri (1.18.10-x86_64-linux-musl) racc (~> 1.4) oauth2 (2.0.15) faraday (>= 0.17.3, < 4.0) diff --git a/app/components/menu.rb b/app/components/menu.rb index d8bd170..231215d 100644 --- a/app/components/menu.rb +++ b/app/components/menu.rb @@ -22,13 +22,34 @@ def view_template if hotwire_native_app? || (@controller_name == "pages" && @action_name == "show") if user_signed_in? - menu_link(path: destroy_user_session_path, text: t("navbar.sign_out"), icon: Hero::ArrowLeftStartOnRectangle.new(variant: :outline, class: "size-5 ltr:transform ltr:-scale-x-100")) - Separator(class: "my-2") + Link( + variant: :link, + href: destroy_user_session_path, + class: "flex items-center justify-start gap-x-2 text-xl text-muted-foreground", + data: { + controller: "bridge--sign-out", + bridge__sign_out_path_value: api_v1_auth_path, + action: [ + ("click->ruby-ui--sheet-content#close" if hotwire_native_app?), + "bridge--sign-out#signOut" + ], + turbo_method: :delete + } + ) do + Hero::ArrowLeftStartOnRectangle(variant: :outline, class: "size-5 ltr:transform ltr:-scale-x-100") + + plain t("navbar.sign_out") + end else - menu_link(path: new_user_session_path, text: t("navbar.sign_in"), icon: Hero::ArrowLeftStartOnRectangle.new(variant: :outline, class: "size-5 ltr:transform ltr:-scale-x-100")) - Separator(class: "my-2") + menu_link( + path: new_user_session_path, + text: t("navbar.sign_in"), + icon: Hero::ArrowLeftOnRectangle.new(variant: :outline, class: "size-5 ltr:transform ltr:-scale-x-100"), + ) end + Separator(class: "my-2") + if hotwire_native_app? menu_link( path: new_contact_path, diff --git a/app/controllers/api/v1/auths_controller.rb b/app/controllers/api/v1/auths_controller.rb new file mode 100644 index 0000000..416eeb2 --- /dev/null +++ b/app/controllers/api/v1/auths_controller.rb @@ -0,0 +1,9 @@ +class Api::V1::AuthsController < ApplicationController + skip_before_action :verify_authenticity_token, only: [ :destroy ] + + def destroy + sign_out(current_user) + + render json: {} + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fcbc2c9..5885c8c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,11 +5,21 @@ class ApplicationController < ActionController::Base # Only allow browsers supporting TailwindCSS v4. allow_browser versions: { chrome: 111, safari: 16.4, firefox: 128 }, block: :handle_outdated_browser + before_action if: -> { devise_controller? && hotwire_native_app? } do + request.env["warden"].params["hotwire_native_form"] = true + end + before_action :check_rack_mini_profiler before_action { Rails.error.set_context(request_url: request.original_url, params: params, session: session.inspect) } private + def after_sign_in_path_for(resource_or_scope) + return "/reset_app" if hotwire_native_app? + + super + end + def check_rack_mini_profiler Rack::MiniProfiler.authorize_request if current_user&.admin? end diff --git a/app/controllers/hotwire/ios/path_configurations_controller.rb b/app/controllers/hotwire/ios/path_configurations_controller.rb new file mode 100644 index 0000000..243669e --- /dev/null +++ b/app/controllers/hotwire/ios/path_configurations_controller.rb @@ -0,0 +1,63 @@ +class Hotwire::Ios::PathConfigurationsController < ApplicationController + def show + render json: { + settings: { + register_with_account: false, + require_authentication: false, + tabs: [ + { + title: "الرئيسية", + path: "/", + ios_system_image_name: "house" + }, + { + title: "التصنيفات", + path: "/categories", + ios_system_image_name: "square.grid.2x2" + }, + { + title: "المؤلفون", + path: "/authors", + ios_system_image_name: "person.3" + }, + { + title: "الكتب", + path: "/books", + ios_system_image_name: "books.vertical" + } + ] + }, + rules: [ + { + patterns: [ + "/new$", + "/edit$", + "/users/sign_up", + "/users/sign_in" + ], + properties: { + context: "modal" + } + }, + { + patterns: [ "^/unauthorized" ], + properties: { + view_controller: "unauthorized" + } + }, + { + patterns: [ "^/reset_app$" ], + properties: { + view_controller: "reset_app" + } + }, + { + patterns: [ "/?sign_in_token=.*" ], + properties: { + presentation: "replace" + } + } + ] + } + end +end diff --git a/app/controllers/native/sessions_controller.rb b/app/controllers/native/sessions_controller.rb index 100ebab..2f8a7e9 100644 --- a/app/controllers/native/sessions_controller.rb +++ b/app/controllers/native/sessions_controller.rb @@ -8,6 +8,6 @@ def handoff sign_in(user) - redirect_to after_sign_in_path_for(user) + redirect_to "/reset_app" end end diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb new file mode 100644 index 0000000..9265f7f --- /dev/null +++ b/app/controllers/site_controller.rb @@ -0,0 +1,8 @@ +class SiteController < ApplicationController + def reset_app + # Hotwire Native needs an empty page to route authentication and reset the app. + # We can't head: 200 because we also need the Turbo JavaScript in . + + render Views::Site::ResetApp.new + end +end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 4e83280..ea3202a 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,4 +1,6 @@ class StaticController < ApplicationController + include Devise::Controllers::Rememberable + CATEGORY_IDS = { faith: [ 24, 34, 35, 36, 37, 38, 39, 40, 54 ], quran: [ 1, 17, 20, 21, 22, 88, 101 ], @@ -10,6 +12,15 @@ class StaticController < ApplicationController } def home + if params[:sign_in_token].present? + user = User.find_signed(params[:sign_in_token], purpose: "native_handoff") + + if user.present? + sign_in(user) + remember_me(user) + end + end + results = search if results.present? && params[:qid].blank? && request.headers["X-Sec-Purpose"] != "prefetch" diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 4de39e0..be3e6c8 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,5 +1,7 @@ module Users class OmniauthCallbacksController < Devise::OmniauthCallbacksController + include Devise::Controllers::Rememberable + def google_oauth2 = process_oauth_callback("Google") private @@ -16,13 +18,15 @@ def process_oauth_callback(provider) def handle_successful_authentication(user, provider) sign_out_all_scopes - sign_in(user) if native_oauth_request? - token = user.signed_id(purpose: "native_handoff", expires_in: 5.minutes) + token = user.signed_id(purpose: "native_handoff", expires_in: 30.seconds) - redirect_to handoff_native_session_url(token:) + redirect_to root_path(sign_in_token: token, locale: nil) else + sign_in(user) + remember_me(user) + flash[:notice] = t("devise.omniauth_callbacks.success", kind: provider) if is_navigational_format? redirect_to after_sign_in_path_for(user) diff --git a/app/javascript/controllers/bridge/sign_out_controller.js b/app/javascript/controllers/bridge/sign_out_controller.js new file mode 100644 index 0000000..914c13b --- /dev/null +++ b/app/javascript/controllers/bridge/sign_out_controller.js @@ -0,0 +1,18 @@ +import { BridgeComponent } from "@hotwired/hotwire-native-bridge" + +// Connects to data-controller="bridge--sign-out" +export default class extends BridgeComponent { + static component = "sign-out" + + static values = { + path: String, + } + + signOut(event) { + event.preventDefault() + event.stopImmediatePropagation() + + const path = this.pathValue + this.send("signOut", { path }, () => {}) + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 179dad0..e7ea3ca 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -19,6 +19,9 @@ application.register("bridge--menu-button", Bridge__MenuButtonController) import Bridge__ShareController from "./bridge/share_controller" application.register("bridge--share", Bridge__ShareController) +import Bridge__SignOutController from "./bridge/sign_out_controller" +application.register("bridge--sign-out", Bridge__SignOutController) + import Bridge__ThemeController from "./bridge/theme_controller" application.register("bridge--theme", Bridge__ThemeController) diff --git a/app/views/devise/registrations/new.rb b/app/views/devise/registrations/new.rb index 3b42752..a1d14d5 100644 --- a/app/views/devise/registrations/new.rb +++ b/app/views/devise/registrations/new.rb @@ -68,8 +68,8 @@ def view_template TextSeparator(text: t("or"), class: "my-2.5") - button_to( - omniauth_authorize_path(resource_name, :google_oauth2), + link_to( + omniauth_authorize_path(resource_name, :google_oauth2, native: hotwire_native_app? ? 1 : 0), class: "inline-flex justify-center items-center py-2 px-5 w-full text-sm text-white rounded-md font-medium focus:outline-hidden bg-[#4285F4] hover:bg-[#4285F4]/90 gap-x-2 cursor-pointer", data: { turbo: "false" } ) do diff --git a/app/views/site/reset_app.rb b/app/views/site/reset_app.rb new file mode 100644 index 0000000..28d4251 --- /dev/null +++ b/app/views/site/reset_app.rb @@ -0,0 +1,4 @@ +class Views::Site::ResetApp < Views::Base + def view_template + end +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c92c50a..70b50ad 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,5 +1,23 @@ # frozen_string_literal: true +# Required to handle tabs that require authentication to present /unauthorized. +class TurboFailureApp < Devise::FailureApp + # Compatibility for Turbo::Native::Navigation + class << self + def helper_method(*methods) + end + end + + include Turbo::Native::Navigation + + # Intercept for Hotwire Native: + # Return a 401 for any :authenticate_user before actions + # Return a 422 for any login failures + def http_auth? + (hotwire_native_app? && !params["hotwire_native_form"]) || super + end +end + # Assuming you have not yet modified this file, each configuration option below # is set to its default value. Note that some are commented out while others # are not: uncommented lines are intended to protect your configuration from @@ -285,10 +303,11 @@ # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # - # config.warden do |manager| - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - # end + config.warden do |manager| + manager.failure_app = TurboFailureApp + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + end # ==> Mountable engine configurations # When using Devise inside an engine, let's call it `MyEngine`, and this engine diff --git a/config/routes.rb b/config/routes.rb index 67cdbf5..3ebadc9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,7 @@ # GET /apple-app-site-association(.:format) rails/pwa#apple_app_site_association # user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) users/omniauth_callbacks#passthru # user_google_oauth2_omniauth_callback GET|POST /users/auth/google_oauth2/callback(.:format) users/omniauth_callbacks#google_oauth2 +# reset_app GET /reset_app(.:format) site#reset_app # pdfjs GET /pdfjs(.:format) pdfjs#index # pdfjs_iframe GET /pdfjs/iframe(.:format) pdfjs#iframe # /404(.:format) errors#not_found @@ -50,7 +51,9 @@ # book_file GET (/:locale)/:book_id/:file_id(.:format) files#show {locale: /ar|ur|en/, book_id: /\d+/, file_id: /\d+/} # book GET (/:locale)/:book_id(.:format) books#show {locale: /ar|ur|en/, book_id: /\d+/} # handoff_native_session GET /native/session/handoff(.:format) native/sessions#handoff +# hotwire_ios_path_configuration GET /hotwire/ios/path_configuration(.:format) hotwire/ios/path_configurations#show # privacy GET /privacy(.:format) static#privacy +# api_v1_auth DELETE /api/v1/auth(.:format) api/v1/auths#destroy # api_v1_libraries GET /api/v1/libraries(.:format) api/v1/libraries#index # api_v1_library GET /api/v1/libraries/:id(.:format) api/v1/libraries#show # api_v1_books GET /api/v1/books(.:format) api/v1/books#index @@ -327,6 +330,8 @@ devise_for :users, only: :omniauth_callbacks, controllers: { omniauth_callbacks: "users/omniauth_callbacks" } + get "reset_app", to: "site#reset_app" + get "pdfjs", to: "pdfjs#index" get "pdfjs/iframe", to: "pdfjs#iframe" @@ -361,10 +366,18 @@ end end + namespace :hotwire do + namespace :ios do + resource :path_configuration, only: :show + end + end + get "/privacy", to: "static#privacy", as: :privacy namespace :api do namespace :v1 do + resource :auth, only: [ :destroy ] + resources :libraries, only: %i[ index show ] resources :books, only: %i[ index show ] resources :authors, only: %i[ index show ]