├── log └── .keep ├── storage └── .keep ├── vendor ├── .keep └── javascript │ └── .keep ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── scrape.rake ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── user_test.rb │ ├── favorite_test.rb │ └── property_test.rb ├── system │ └── .keep ├── controllers │ ├── .keep │ └── properties_controller_test.rb ├── integration │ └── .keep ├── fixtures │ ├── files │ │ └── .keep │ ├── users.yml │ ├── favorites.yml │ └── properties.yml ├── application_system_test_case.rb └── test_helper.rb ├── .ruby-version ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── images │ │ └── .keep │ ├── stylesheets │ │ └── application.tailwind.scss │ └── config │ │ └── manifest.js ├── models │ ├── concerns │ │ └── .keep │ ├── admin.rb │ ├── application_record.rb │ ├── property.rb │ ├── favorite.rb │ ├── user.rb │ └── ability.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── application_controller.rb │ ├── properties_controller.rb │ └── users │ │ └── sessions_controller.rb ├── javascript │ ├── application.js │ ├── configs │ │ └── base_url.js │ ├── routes │ │ ├── users │ │ │ ├── sign_up.js │ │ │ └── sign_in.js │ │ ├── users.js │ │ ├── favorites.js │ │ └── properties.js │ ├── entrypoint.jsx │ └── components │ │ ├── app.test.js │ │ ├── layouts │ │ ├── navbar.jsx │ │ ├── login_button.jsx │ │ ├── pagination.jsx │ │ └── query_form.jsx │ │ ├── properties │ │ ├── property_item.jsx │ │ └── property_list.jsx │ │ ├── app.jsx │ │ └── favorites │ │ └── favorite_button.jsx ├── views │ ├── properties │ │ └── index.html.erb │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ ├── react_app.html.erb │ │ └── application.html.erb │ └── devise │ │ ├── mailer │ │ ├── password_change.html.erb │ │ ├── confirmation_instructions.html.erb │ │ ├── unlock_instructions.html.erb │ │ ├── email_changed.html.erb │ │ └── reset_password_instructions.html.erb │ │ ├── shared │ │ ├── _error_messages.html.erb │ │ └── _links.html.erb │ │ ├── unlocks │ │ └── new.html.erb │ │ ├── passwords │ │ ├── new.html.erb │ │ └── edit.html.erb │ │ ├── confirmations │ │ └── new.html.erb │ │ ├── sessions │ │ └── new.html.erb │ │ └── registrations │ │ ├── new.html.erb │ │ └── edit.html.erb ├── helpers │ ├── properties_helper.rb │ └── application_helper.rb ├── mailers │ └── application_mailer.rb ├── api │ ├── api │ │ ├── base.rb │ │ ├── authorization.rb │ │ └── has_response.rb │ └── v1 │ │ ├── properties.rb │ │ └── favorites.rb ├── jobs │ └── application_job.rb └── scrapers │ └── house_scraper.rb ├── .rspec ├── Procfile.dev ├── bin ├── rake ├── importmap ├── rails ├── dev ├── setup └── bundle ├── spec ├── support │ └── factory_bot.rb ├── factories │ ├── favorite.rb │ ├── property.rb │ └── user.rb ├── rails_helper.rb ├── api │ └── v1 │ │ └── favorites_spec.rb └── spec_helper.rb ├── config ├── environment.rb ├── initializers │ ├── session_store.rb │ ├── filter_parameter_logging.rb │ ├── permissions_policy.rb │ ├── assets.rb │ ├── inflections.rb │ ├── rails_admin.rb │ ├── content_security_policy.rb │ ├── devise.rb │ └── doorkeeper.rb ├── boot.rb ├── cable.yml ├── routes.rb ├── credentials.yml.enc ├── database.yml ├── application.rb ├── locales │ ├── en.yml │ ├── devise.en.yml │ └── doorkeeper.en.yml ├── storage.yml ├── puma.rb └── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── db ├── migrate │ ├── 20220616132910_add_column_to_users.rb │ ├── 20220616083407_add_index_on_properties.rb │ ├── 20220621155120_create_table_favorites.rb │ ├── 20220614142447_create_properties.rb │ ├── 20220613160813_devise_create_users.rb │ └── 20220626144233_create_doorkeeper_tables.rb ├── seeds.rb └── schema.rb ├── config.ru ├── tailwind.config.js ├── .gitattributes ├── Rakefile ├── .eslintrc.yml ├── .rubocop.yml ├── README.md ├── .gitignore ├── package.json ├── Gemfile ├── jest.config.js └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.2 2 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/properties/index.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/models/admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Admin < User 4 | end 5 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | js: yarn build --watch 3 | css: yarn build:css --watch 4 | -------------------------------------------------------------------------------- /app/helpers/properties_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PropertiesHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.tailwind.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/javascript/configs/base_url.js: -------------------------------------------------------------------------------- 1 | export default { 2 | baseUrl: `${window.location.protocol}//${window.location.hostname}` 3 | } 4 | -------------------------------------------------------------------------------- /app/javascript/routes/users/sign_up.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SignUp = () => { 4 | }; 5 | 6 | export default SignUp; 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | primary_abstract_class 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.include FactoryBot::Syntax::Methods 5 | end 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /lib/tasks/scrape.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :crawl do 4 | task house: :environment do 5 | HouseScraper.new.crawl! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/favorite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :favorite do 5 | user 6 | property 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_tree ../../javascript .js 3 | //= link_tree ../../../vendor/javascript .js 4 | //= link_tree ../builds 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/models/property.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Property < ApplicationRecord 4 | has_many :favorites 5 | has_many :users, through: :favorites 6 | end 7 | -------------------------------------------------------------------------------- /app/views/devise/mailer/password_change.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

We're contacting you to notify you that your password has been changed.

4 | -------------------------------------------------------------------------------- /db/migrate/20220616132910_add_column_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddColumnToUsers < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :users, :type, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/properties_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PropertiesController < ApplicationController 4 | layout 'react_app' 5 | 6 | def index; end 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! command -v foreman &> /dev/null 4 | then 5 | echo "Installing foreman..." 6 | gem install foreman 7 | fi 8 | 9 | foreman start -f Procfile.dev "$@" 10 | -------------------------------------------------------------------------------- /app/models/favorite.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Favorite < ApplicationRecord 4 | belongs_to :user 5 | belongs_to :property 6 | 7 | validates :user_id, :property_id, presence: true 8 | end 9 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class UserTest < ActiveSupport::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | Seedhouse::Application.config.session_store :redis_store, 2 | servers: ["redis://localhost:6379/0/session"], 3 | expire_after: 90.minutes, 4 | threadsafe: true, 5 | secure: true 6 | -------------------------------------------------------------------------------- /test/models/favorite_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class FavoriteTest < ActiveSupport::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/models/property_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class PropertyTest < ActiveSupport::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | Rails.application.load_server 9 | -------------------------------------------------------------------------------- /app/api/api/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module API 4 | class Base < Grape::API 5 | prefix '/api' 6 | format :json 7 | 8 | mount V1::Properties 9 | mount V1::Favorites 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 6 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20220616083407_add_index_on_properties.rb: -------------------------------------------------------------------------------- 1 | class AddIndexOnProperties < ActiveRecord::Migration[7.0] 2 | def change 3 | add_index :properties, [:title, :address_line], unique: true 4 | add_index :properties, :address_district 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/properties_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class PropertiesControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './app/views/**/*.html.erb', 4 | './app/helpers/**/*.rb', 5 | './app/assets/stylesheets/**/*.css', 6 | './app/javascript/**/*.js', 7 | './app/javascript/**/*.jsx' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @email %>!

2 | 3 |

You can confirm your account email through the link below:

4 | 5 |

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: redis://localhost:6379/1 4 | 5 | test: 6 | adapter: test 7 | 8 | production: 9 | adapter: redis 10 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 11 | channel_prefix: seedhouse_production 12 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20220621155120_create_table_favorites.rb: -------------------------------------------------------------------------------- 1 | class CreateTableFavorites < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :favorites do |t| 4 | t.integer :user_id, index: true 5 | t.integer :property_id, index: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/javascript/entrypoint.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./components/app.jsx"; 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | const rootEl = document.getElementById('app'); 7 | const root = ReactDOM.createRoot(rootEl); 8 | root.render(); 9 | }) 10 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 |

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

8 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - eslint:recommended 6 | - plugin:react/recommended 7 | - plugin:react/jsx-runtime 8 | parserOptions: 9 | ecmaFeatures: 10 | jsx: true 11 | ecmaVersion: latest 12 | sourceType: module 13 | plugins: 14 | - react 15 | rules: {} 16 | settings: {react: {version: detect} } 17 | -------------------------------------------------------------------------------- /app/views/devise/mailer/email_changed.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @email %>!

2 | 3 | <% if @resource.try(:unconfirmed_email?) %> 4 |

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

5 | <% else %> 6 |

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/javascript/routes/users.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | import Navbar from '../components/layouts/navbar.jsx'; 4 | 5 | const Users = () => { 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | }; 13 | 14 | export default Users; 15 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount RailsAdmin::Engine => '/admin', as: 'rails_admin' 3 | mount API::Base => "/" 4 | use_doorkeeper do 5 | skip_controllers :authorizations, :applications, :authorized_applications 6 | end 7 | 8 | devise_for :users, controllers: { sessions: 'users/sessions' } 9 | 10 | root to: "properties#index" 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the "{}" from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/favorites.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the "{}" from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/properties.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the "{}" from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /app/javascript/components/app.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | import App from './app.jsx'; 4 | 5 | 6 | describe('Render sign in button or sign out button', () => { 7 | test('when user has not logged in it should display sign in button', () => { 8 | render( 9 | 10 | ); 11 | expect(screen.queryByText('Sign In')).toBeTruthy; 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Someone has requested a link to change your password. You can do this through the link below.

4 | 5 |

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

6 | 7 |

If you didn't request this, please ignore this email.

8 |

Your password won't change until you access the link above and create a new one.

9 | -------------------------------------------------------------------------------- /app/api/api/authorization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module API 4 | module Authorization 5 | def authenticate! 6 | error!('Not authorized!', 401) unless access_token && current_user 7 | end 8 | 9 | def current_user 10 | @current_user ||= User.find(Doorkeeper::AccessToken.by_token(access_token).resource_owner_id) 11 | end 12 | 13 | def access_token 14 | request.headers['Authorization'] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | zW+zb6QZMqoEApo39YYGnUq+Mlp3dOHL7wZdVhFttjBeIlZ/Skxv4Briu2imh4B6hZSYNBZbA9eA6eZu9hwpOdC22xqrA1S/Ahxc0fHhivrqC/ATsfVAGgdt6QgMAcy9+Bu4tttuyuD12mQaej0P/NKQZzg3K7Ct2Uq1Gi/ifTegRtQwq3M3ZN7u+HO2m21/RWZAtDVe5c4DjdDXrN+VYOrnJhG0JNJxsU/vPyWzspexF4BfIqR8Lc4Akx8x9BAIC6fKhzSbRHw2Hx0SDHXZ7RGtOHZ3S8TBMhRrN9kP0+sfsHdLykMpXmGMDWq1mThAy7taIdBPMhjPxcYqsbkJ1wLNITz5oaPu6qwU2OKsu/PA4vUivSkXtZKQZuXp1AAuMVDmyE1NkeZC3BZKDDRvEj0hcz44fNnT7CBU--GAmf8WKjZSOgNl+L--7Ocq3zixtKytDQsRIo+sVA== -------------------------------------------------------------------------------- /app/views/devise/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if resource.errors.any? %> 2 |
3 |

4 | <%= I18n.t("errors.messages.not_saved", 5 | count: resource.errors.count, 6 | resource: resource.class.model_name.human.downcase) 7 | %> 8 |

9 |
    10 | <% resource.errors.full_messages.each do |message| %> 11 |
  • <%= message %>
  • 12 | <% end %> 13 |
14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /spec/factories/property.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :property do 5 | title { Faker::Books::Lovecraft.location } 6 | address_city { Faker::Address.city } 7 | address_district { Faker::Address.street_address } 8 | address_line { Faker::Address.full_address } 9 | amount_in_cent { Faker::Currency.rand_in_range(1, 90_000) } 10 | room { Faker::Number.rand_in_range(1, 10) } 11 | mrt_line { Faker::Color.color_name } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20220614142447_create_properties.rb: -------------------------------------------------------------------------------- 1 | class CreateProperties < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :properties do |t| 4 | t.integer :user_id 5 | t.string :title 6 | t.integer :amount_in_cent, null: false 7 | t.string :address_district 8 | t.string :address_city 9 | t.string :address_line 10 | t.string :mrt_line 11 | t.integer :room 12 | t.string :image 13 | 14 | t.timestamps 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | 9 | Admin.create(email: 'test@gmail.com', password: '123456') 10 | 11 | HouseScraper.new.crawl! 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require_relative '../config/environment' 5 | require 'rails/test_help' 6 | 7 | module ActiveSupport 8 | class TestCase 9 | # Run tests in parallel with specified workers 10 | parallelize(workers: :number_of_processors) 11 | 12 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 13 | fixtures :all 14 | 15 | # Add more helper methods to be used by all tests here... 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/layouts/react_app.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Seedhouse 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> 10 | <%= javascript_include_tag "entrypoint", "data-turbo-track": "reload", defer: true %> 11 | 12 | 13 | 14 |
15 | <%= yield %> 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Seedhouse 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> 10 | <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> 11 | 12 | 13 | 14 |
15 | <%= yield %> 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :user do 5 | email { Faker::Internet.email } 6 | password { Faker::Crypto.sha256 } 7 | 8 | trait :with_properties do 9 | property 10 | end 11 | 12 | factory :user_with_favorites do 13 | transient do 14 | favorites_count { 3 } 15 | end 16 | 17 | after(:create) do |user, evaluator| 18 | create_list(:favorite, evaluator.favorites_count, user: user) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/javascript/components/layouts/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import LoginButton from '../layouts/login_button.jsx' 4 | 5 | const Navbar = () => { 6 | return ( 7 |
8 |
    9 | Property 10 | Favorite 11 | 12 |
13 |
14 | ) 15 | }; 16 | 17 | export default Navbar; 18 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend unlock instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 |
12 | <%= f.submit "Resend unlock instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

Forgot your password?

2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 |
12 | <%= f.submit "Send me reset password instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | # Include default devise modules. Others available are: 5 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable 6 | devise :database_authenticatable, :registerable, 7 | :recoverable, :rememberable, :validatable 8 | 9 | has_many :properties 10 | has_many :favorites 11 | has_many :favorite_properties, through: :favorites, source: :property 12 | 13 | def self.authenticate(email, password) 14 | user = User.find_for_authentication(email: email) 15 | user&.valid_password?(password) ? user : nil 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/api/api/has_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module API 4 | module HasResponse 5 | def respond(data:, error: nil) 6 | return respond_failure(error) if error 7 | 8 | respond_success(data) 9 | end 10 | 11 | def respond_success(data) 12 | { 13 | return_status: { 14 | code: 200, 15 | message: 'success' 16 | }, 17 | data: data 18 | } 19 | end 20 | 21 | def respond_failure(error) 22 | { 23 | return_status: { 24 | code: error.code, 25 | message: error.error_message 26 | } 27 | } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> 9 |
10 | 11 |
12 | <%= f.submit "Resend confirmation instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | inflect.acronym "API" 16 | end 17 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: postgresql 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: seedhouse_development 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: seedhouse_test 22 | 23 | production: 24 | <<: *default 25 | database: seedhouse_production 26 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # The behavior of RuboCop can be controlled via the .rubocop.yml 2 | # configuration file. It makes it possible to enable/disable 3 | # certain cops (checks) and to alter their behavior if they accept 4 | # any parameters. The file can be placed either in your home 5 | # directory or in some project directory. 6 | # 7 | # RuboCop will start looking for the configuration file in the directory 8 | # where the inspected file is and continue its way up to the root directory. 9 | # 10 | # See https://docs.rubocop.org/rubocop/configuration 11 | 12 | Style/Documentation: 13 | Enabled: false 14 | AllCops: 15 | NewCops: enable 16 | Exclude: 17 | - 'db/**/*' 18 | - 'config/**/*' 19 | - 'script/**/*' 20 | - 'bin/*' 21 | 22 | -------------------------------------------------------------------------------- /app/controllers/users/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Users::SessionsController < Devise::SessionsController 4 | # before_action :configure_sign_in_params, only: [:create] 5 | skip_before_action :verify_authenticity_token, on: [:create] 6 | 7 | # GET /resource/sign_in 8 | def new 9 | super 10 | end 11 | 12 | # POST /resource/sign_in 13 | def create 14 | super 15 | end 16 | 17 | # DELETE /resource/sign_out 18 | def destroy 19 | super 20 | end 21 | 22 | # protected 23 | 24 | # If you have extra params to permit, append them to the sanitizer. 25 | # def configure_sign_in_params 26 | # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) 27 | # end 28 | end 29 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

Log in

2 | 3 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> 4 |
5 | <%= f.label :email %>
6 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 7 |
8 | 9 |
10 | <%= f.label :password %>
11 | <%= f.password_field :password, autocomplete: "current-password" %> 12 |
13 | 14 | <% if devise_mapping.rememberable? %> 15 | 19 | <% end %> 20 | 21 |
22 | <%= f.submit "Log in" %> 23 |
24 | <% end %> 25 | 26 | <%= render "devise/shared/links" %> 27 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Seedhouse 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 7.0 13 | 14 | # Configuration for the application, engines, and railties goes here. 15 | # 16 | # These settings can be overridden in specific environments using the files 17 | # in config/environments, which are processed later. 18 | # 19 | # config.time_zone = "Central Time (US & Canada)" 20 | # config.eager_load_paths << Rails.root.join("extras") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/javascript/components/layouts/login_button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Link, 4 | useNavigate 5 | } from 'react-router-dom'; 6 | import { useQueryClient } from 'react-query'; 7 | 8 | const LoginButton = () => { 9 | const queryClient = useQueryClient(); 10 | const navigate = useNavigate(); 11 | const accessToken = queryClient.getQueryData('accessToken'); 12 | 13 | const clearAccessToken = () => { 14 | queryClient.setQueryData('accessToken', undefined); 15 | navigate('/'); 16 | }; 17 | 18 | const signInButton = Sign In 19 | const signOutButton = 20 | 21 | 22 | return ( 23 |
24 | { accessToken === undefined ? signInButton : signOutButton } 25 |
26 | ) 27 | }; 28 | 29 | export default LoginButton; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | ## Environment 4 | * Ruby Version: 3.0.2 5 | * Rails Version: 7.0.3 6 | * React Version: 18.1.0 7 | * Database: PostgreSQL 8 | 9 | ## Setup Project 10 | 1. Use `rbenv` or `rvm` to install Ruby 3.0.2 11 | 2. Install bundler: `gem install bundler` 12 | 3. Install Ruby dependencies: `bundle install` 13 | 4. Install Node Package dependencies: `yarn` 14 | 5. Create database: `bundle exec rails db:create` 15 | 6. Run migrations: `bundle exec rails db:migrate` 16 | 7. Generate mock data for database: `bundle exec rake db:seed`. It takes few seconds to crawl data from target website and generate admin account 17 | 7. Launch servers: `./bin/dev` 18 | 8. Visit Rails app: `http://localhost:3000` 19 | 20 | ## Admin User 21 | 1. After `db:seed` is successful, sign in with below admin account and password 22 | * account: `test@gmail.com` 23 | * password: `123456` 24 | 2. visit `http://localhost:3000/admin` to access administration page to manage data of the app 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | /tmp/* 22 | 23 | # Ignore uploaded files in development. 24 | /storage/* 25 | !/storage/.keep 26 | /tmp/storage/* 27 | !/tmp/storage/ 28 | !/tmp/storage/.keep 29 | 30 | /public/assets 31 | 32 | # Ignore master key for decrypting credentials and more. 33 | /config/master.key 34 | 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | 38 | /node_modules 39 | /coverage 40 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Change your password

2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | <%= f.hidden_field :reset_password_token %> 6 | 7 |
8 | <%= f.label :password, "New password" %>
9 | <% if @minimum_password_length %> 10 | (<%= @minimum_password_length %> characters minimum)
11 | <% end %> 12 | <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> 13 |
14 | 15 |
16 | <%= f.label :password_confirmation, "Confirm new password" %>
17 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 18 |
19 | 20 |
21 | <%= f.submit "Change my password" %> 22 |
23 | <% end %> 24 | 25 | <%= render "devise/shared/links" %> 26 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign up

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 |
12 | <%= f.label :password %> 13 | <% if @minimum_password_length %> 14 | (<%= @minimum_password_length %> characters minimum) 15 | <% end %>
16 | <%= f.password_field :password, autocomplete: "new-password" %> 17 |
18 | 19 |
20 | <%= f.label :password_confirmation %>
21 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 22 |
23 | 24 |
25 | <%= f.submit "Sign up" %> 26 |
27 | <% end %> 28 | 29 | <%= render "devise/shared/links" %> 30 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/api/v1/properties.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module V1 4 | class Properties < Grape::API 5 | helpers API::HasResponse 6 | 7 | helpers do 8 | params :pagination do 9 | optional :page, type: Integer 10 | end 11 | end 12 | 13 | desc 'Properties data' 14 | params do 15 | use :pagination, default_page: 1 16 | end 17 | get '/v1/properties' do 18 | query_result = Property 19 | .ransack(title_cont_any: params[:title], address_city_cont_any: params[:address_city]) 20 | .result 21 | .order(amount_in_cent: :asc) 22 | .page(params[:page]) 23 | .per(8) 24 | response_data = { 25 | items: query_result, 26 | pagination: { 27 | total_pages: query_result.total_pages, 28 | current_page: query_result.current_page 29 | } 30 | } 31 | respond(data: response_data) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /config/initializers/rails_admin.rb: -------------------------------------------------------------------------------- 1 | RailsAdmin.config do |config| 2 | config.asset_source = :sprockets 3 | 4 | ### Popular gems integration 5 | 6 | ## == Devise == 7 | config.authenticate_with do 8 | warden.authenticate! scope: :user 9 | end 10 | config.current_user_method(&:current_user) 11 | 12 | ## == CancanCan == 13 | config.authorize_with :cancancan 14 | 15 | ## == Pundit == 16 | # config.authorize_with :pundit 17 | 18 | ## == PaperTrail == 19 | # config.audit_with :paper_trail, 'User', 'PaperTrail::Version' # PaperTrail >= 3.0.0 20 | 21 | ### More at https://github.com/railsadminteam/rails_admin/wiki/Base-configuration 22 | 23 | ## == Gravatar integration == 24 | ## To disable Gravatar integration in Navigation Bar set to false 25 | # config.show_gravatar = true 26 | 27 | config.actions do 28 | dashboard # mandatory 29 | index # mandatory 30 | new 31 | export 32 | bulk_delete 33 | show 34 | edit 35 | delete 36 | show_in_app 37 | 38 | ## With an audit adapter, you can add: 39 | # history_index 40 | # history_show 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /app/javascript/components/properties/property_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import FavoriteButton from '../favorites/favorite_button.jsx' 4 | 5 | const PropertyItem = (props) => { 6 | const [isFavorite, setIsFavorite] = useState(props.isFavorite); 7 | return ( 8 |
9 | 10 | image 11 | 12 |
{props.data.title}
13 |
14 |
15 | NT$ {props.data.amount_in_cent / 100 * 30} 16 |
17 | / month 18 |
19 |
20 | {props.data.address_line} 21 |
22 |
23 | { 24 | props.data.mrt_line ?
MRT: {props.data.mrt_line}
: "" 25 | } 26 |
27 | ) 28 | }; 29 | 30 | export default PropertyItem; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": "true", 4 | "scripts": { 5 | "build": "esbuild app/javascript/entrypoint.jsx --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --loader:.js=jsx", 6 | "watch": "esbuild app/javascript/*.* --watch --bundle --outdir=app/assets/builds --loader:.js=jsx", 7 | "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.scss -o ./app/assets/builds/application.css --minify", 8 | "test": "jest" 9 | }, 10 | "dependencies": { 11 | "autoprefixer": "^10.4.7", 12 | "esbuild": "^0.14.43", 13 | "postcss": "^8.4.14", 14 | "react": "^18.1.0", 15 | "react-dom": "^18.1.0", 16 | "react-hook-form": "^7.33.0", 17 | "react-query": "^3.39.1", 18 | "react-router": "^6.3.0", 19 | "react-router-dom": "6", 20 | "tailwindcss": "^3.1.2" 21 | }, 22 | "devDependencies": { 23 | "@testing-library/jest-dom": "^5.16.4", 24 | "@testing-library/react": "^13.3.0", 25 | "@testing-library/react-hooks": "^8.0.1", 26 | "esbuild-jest": "^0.5.0", 27 | "eslint": "^8.17.0", 28 | "eslint-plugin-react": "^7.30.0", 29 | "jest": "^28.1.2", 30 | "jest-environment-jsdom": "^28.1.2", 31 | "react-test-renderer": "^18.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end %> 4 | 5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end %> 8 | 9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end %> 12 | 13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end %> 16 | 17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end %> 20 | 21 | <%- if devise_mapping.omniauthable? %> 22 | <%- resource_class.omniauth_providers.each do |provider| %> 23 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
24 | <% end %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/javascript/components/layouts/pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | useState, 4 | useEffect 5 | } from 'react'; 6 | 7 | const Pagination = (props) => { 8 | if (props.currentPage === null || props.totalPages === null) return; 9 | 10 | const totalPages = props.totalPage; 11 | const [currentPage, setCurrentPage] = useState(props.currentPage); 12 | 13 | useEffect(() => { 14 | setCurrentPage(props.currentPage); 15 | }); 16 | 17 | const prevPageHandler = () => { 18 | props.onChangeCurrentPage(currentPage - 1); 19 | }; 20 | 21 | const nextPageHandler = () => { 22 | props.onChangeCurrentPage(currentPage + 1); 23 | }; 24 | 25 | return ( 26 |
27 | { currentPage === 1 ?
Prev
: } 28 |
Page: {currentPage}
29 | { currentPage < totalPages ? :
Next
} 30 |
31 | ) 32 | }; 33 | 34 | export default Pagination; 35 | -------------------------------------------------------------------------------- /app/javascript/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter, 4 | Routes, 5 | Route 6 | } from 'react-router-dom'; 7 | import { 8 | useQuery, 9 | useQueryClient, 10 | QueryClient, 11 | QueryClientProvider 12 | } from 'react-query'; 13 | 14 | import Favorites from '../routes/favorites.js' 15 | import Properties from '../routes/properties.js' 16 | import Users from '../routes/users.js' 17 | import SignIn from '../routes/users/sign_in.js' 18 | import SignUp from '../routes/users/sign_up.js' 19 | 20 | const queryClient = new QueryClient(); 21 | 22 | const App = () => { 23 | return ( 24 | 25 | 26 | 27 | 28 | } /> 29 | } /> 30 | } /> 31 | }> 32 | } /> 33 | } /> 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | }; 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /app/models/ability.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Ability 4 | include CanCan::Ability 5 | 6 | def initialize(user) 7 | # Define abilities for the passed in user here. For example: 8 | # 9 | # user ||= User.new # guest user (not logged in) 10 | # if user.admin? 11 | # can :manage, :all 12 | # else 13 | # can :read, :all 14 | # end 15 | # 16 | # The first argument to `can` is the action you are giving the user 17 | # permission to do. 18 | # If you pass :manage it will apply to every action. Other common actions 19 | # here are :read, :create, :update and :destroy. 20 | # 21 | # The second argument is the resource the user can perform the action on. 22 | # If you pass :all it will apply to every resource. Otherwise pass a Ruby 23 | # class of the resource. 24 | # 25 | # The third argument is an optional hash of conditions to further filter the 26 | # objects. 27 | # For example, here the user can only update published articles. 28 | # 29 | # can :update, Article, :published => true 30 | # 31 | # See the wiki for details: 32 | # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities 33 | return unless user.is_a?(Admin) 34 | 35 | can :access, :rails_admin 36 | can :manage, :all 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/javascript/components/layouts/query_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | useSearchParams, 4 | Link 5 | } from 'react-router-dom'; 6 | 7 | const QueryForm = (props) => { 8 | let [searchParams, setSearchParams] = useSearchParams(); 9 | 10 | const handleSubmit = event => { 11 | event.preventDefault(); 12 | 13 | let params = serializeFromQuery(event.target); 14 | props.onChangeSearchParams(params); 15 | }; 16 | 17 | const serializeFromQuery = (target) => { 18 | const {title, city} = target.elements; 19 | 20 | return {title: title.value, city: city.value} 21 | }; 22 | 23 | return ( 24 |
25 |
26 | 27 | 28 | 29 | 33 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default QueryForm; 44 | -------------------------------------------------------------------------------- /app/javascript/routes/favorites.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useEffect from 'react'; 3 | import Navbar from '../components/layouts/navbar.jsx'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { useQuery, useQueryClient } from 'react-query'; 6 | import PropertyList from '../components/properties/property_list.jsx' 7 | 8 | const Favorites = () => { 9 | const navigate = useNavigate(); 10 | const queryClient = useQueryClient(); 11 | const accessToken = queryClient.getQueryData('accessToken'); 12 | 13 | if (accessToken === undefined) { 14 | return ( 15 |
16 | 17 |
Please sign in to get your favorites.
18 |
19 | ); 20 | }; 21 | 22 | const fetchFavorites = async () => { 23 | return await fetch('api/v1/favorites', { 24 | headers: { 25 | 'authorization': accessToken 26 | } 27 | }).then(data => data.json()); 28 | }; 29 | 30 | useQuery(['favorites'], fetchFavorites); 31 | const data = queryClient.getQueryData('favorites'); 32 | 33 | return ( 34 |
35 | 36 |
37 |
My Favorites
38 |
39 | { data?.data?.items?.length === 0 ?
Grab some favorites...
: } 40 |
41 | ); 42 | }; 43 | 44 | export default Favorites; 45 | -------------------------------------------------------------------------------- /app/api/v1/favorites.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module V1 4 | class Favorites < Grape::API 5 | helpers API::HasResponse 6 | helpers API::Authorization 7 | 8 | before { authenticate! } 9 | 10 | desc 'Get favorites by user' 11 | get 'v1/favorites' do 12 | query_result = current_user 13 | .favorite_properties 14 | .order(id: :asc) 15 | response_data = { 16 | items: query_result 17 | } 18 | 19 | respond(data: response_data) 20 | end 21 | 22 | desc 'Create favorites by user' 23 | post 'v1/favorites' do 24 | favorite = current_user.favorites.find_or_create_by(property_id: params[:property_id]) 25 | user_favorite_property_ids = current_user.favorites.pluck(:property_id) 26 | 27 | response_data = { 28 | favorite_property_ids: user_favorite_property_ids, 29 | message: favorite ? 'success' : 'failed to create' 30 | } 31 | 32 | respond(data: response_data) 33 | end 34 | 35 | desc 'Delete favorites by user' 36 | delete 'v1/favorites' do 37 | destroyed_list = current_user.favorites.destroy_by(property_id: params[:property_id]) 38 | user_favorite_property_ids = current_user.favorites.pluck(:property_id) 39 | 40 | response_data = { 41 | favorite_property_ids: user_favorite_property_ids, 42 | message: destroyed_list.present? ? 'success' : 'favorite dose not exist' 43 | } 44 | 45 | respond(data: response_data) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /db/migrate/20220613160813_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateUsers < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :users do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: "" 8 | t.string :encrypted_password, null: false, default: "" 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | # t.integer :sign_in_count, default: 0, null: false 19 | # t.datetime :current_sign_in_at 20 | # t.datetime :last_sign_in_at 21 | # t.string :current_sign_in_ip 22 | # t.string :last_sign_in_ip 23 | 24 | ## Confirmable 25 | # t.string :confirmation_token 26 | # t.datetime :confirmed_at 27 | # t.datetime :confirmation_sent_at 28 | # t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | # t.string :unlock_token # Only if unlock strategy is :email or :both 33 | # t.datetime :locked_at 34 | 35 | 36 | t.timestamps null: false 37 | end 38 | 39 | add_index :users, :email, unique: true 40 | add_index :users, :reset_password_token, unique: true 41 | # add_index :users, :confirmation_token, unique: true 42 | # add_index :users, :unlock_token, unique: true 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/javascript/components/properties/property_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { 4 | useQuery, 5 | useQueryClient 6 | } from 'react-query'; 7 | 8 | import PropertyItem from './property_item.jsx' 9 | 10 | 11 | const PropertyList = (props) => { 12 | const queryClient = useQueryClient(); 13 | const accessToken = queryClient.getQueryData('accessToken'); 14 | 15 | const fetchFavoriteIds = async () => { 16 | return await fetch('api/v1/favorites', { 17 | headers: { 18 | 'authorization': accessToken 19 | } 20 | }).then(data => data.json()) 21 | .then(data => data?.data?.items?.map(item => item.id)) 22 | }; 23 | 24 | if (accessToken != undefined) { 25 | useQuery(['favoriteIds'], fetchFavoriteIds) 26 | } 27 | 28 | if (props.data === null) return 29 | 30 | const listItems = (props) => { 31 | if (props.data != null) { 32 | const favoriteIds = queryClient.getQueryData('favoriteIds') || []; 33 | return props.data.data.items.map((item, index) => { 34 | if (accessToken) { 35 | const isFavorite = favoriteIds.find(id => id === item.id) ? true : false; 36 | return ; 37 | } else { 38 | return ; 39 | } 40 | }); 41 | } else { 42 | return
Not found.
; 43 | } 44 | } 45 | 46 | return ( 47 |
48 | {listItems(props)} 49 |
50 | ); 51 | } 52 | 53 | export default PropertyList; 54 | -------------------------------------------------------------------------------- /app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit <%= resource_name.to_s.humanize %>

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= render "devise/shared/error_messages", resource: resource %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %> 9 |
10 | 11 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 12 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
13 | <% end %> 14 | 15 |
16 | <%= f.label :password %> (leave blank if you don't want to change it)
17 | <%= f.password_field :password, autocomplete: "new-password" %> 18 | <% if @minimum_password_length %> 19 |
20 | <%= @minimum_password_length %> characters minimum 21 | <% end %> 22 |
23 | 24 |
25 | <%= f.label :password_confirmation %>
26 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %> 27 |
28 | 29 |
30 | <%= f.label :current_password %> (we need your current password to confirm your changes)
31 | <%= f.password_field :current_password, autocomplete: "current-password" %> 32 |
33 | 34 |
35 | <%= f.submit "Update" %> 36 |
37 | <% end %> 38 | 39 |

Cancel my account

40 | 41 |

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

42 | 43 | <%= link_to "Back", :back %> 44 | -------------------------------------------------------------------------------- /db/migrate/20220626144233_create_doorkeeper_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDoorkeeperTables < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :oauth_access_tokens do |t| 6 | t.references :resource_owner, index: true 7 | 8 | # If you use a custom token generator you may need to change this column 9 | # from string to text, so that it accepts tokens larger than 255 10 | # characters. More info on custom token generators in: 11 | # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator 12 | # 13 | # t.text :token, null: false 14 | t.string :token, null: false 15 | 16 | t.integer :application_id 17 | t.string :refresh_token 18 | t.integer :expires_in 19 | t.datetime :revoked_at 20 | t.datetime :created_at, null: false 21 | t.string :scopes 22 | 23 | # The authorization server MAY issue a new refresh token, in which case 24 | # *the client MUST discard the old refresh token* and replace it with the 25 | # new refresh token. The authorization server MAY revoke the old 26 | # refresh token after issuing a new refresh token to the client. 27 | # @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 28 | # 29 | # Doorkeeper implementation: if there is a `previous_refresh_token` column, 30 | # refresh tokens will be revoked after a related access token is used. 31 | # If there is no `previous_refresh_token` column, previous tokens are 32 | # revoked as soon as a new access token is created. 33 | # 34 | # Comment out this line if you want refresh tokens to be instantly 35 | # revoked after use. 36 | end 37 | 38 | add_index :oauth_access_tokens, :token, unique: true 39 | add_index :oauth_access_tokens, :refresh_token, unique: true 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/javascript/routes/properties.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | useState, 4 | useEffect 5 | } from 'react'; 6 | import { 7 | generatePath, 8 | useParams 9 | } from 'react-router'; 10 | import { useSearchParams } from 'react-router-dom'; 11 | import Navbar from '../components/layouts/navbar.jsx'; 12 | import Pagination from '../components/layouts/pagination.jsx'; 13 | import PropertyList from '../components/properties/property_list.jsx'; 14 | import QueryForm from '../components/layouts/query_form.jsx'; 15 | 16 | const Properties = () => { 17 | const [data, setData] = useState(); 18 | const [currentPage, setCurrentPage] = useState(1); 19 | const [totalPages, setTotalPages] = useState(); 20 | let [searchParams, setSearchParams] = useSearchParams(); 21 | 22 | useEffect(() => { 23 | fetchProperties(); 24 | }, [currentPage, searchParams]); 25 | 26 | const fetchProperties = () => { 27 | let queryParams = new URLSearchParams({ 28 | page: currentPage, 29 | title: searchParams.get('title') || '', 30 | address_city: searchParams.get('city') || '', 31 | }) 32 | const url = generatePath('api/v1/properties?') + queryParams; 33 | 34 | fetch(url) 35 | .then(res => res.json()) 36 | .then((data) => { 37 | setData(data); 38 | setTotalPages(data.data.pagination.total_pages); 39 | }) 40 | .catch(e => { 41 | console.log(e); 42 | }); 43 | }; 44 | 45 | return ( 46 |
47 | 48 | setSearchParams(params)}/> 49 | 50 | setCurrentPage(page)} 54 | /> 55 |
56 | ) 57 | }; 58 | 59 | export default Properties; 60 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `bin/rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/javascript/routes/users/sign_in.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useForm } from 'react-hook-form'; 4 | import { 5 | useQuery, 6 | useQueryClient, 7 | useMutation, 8 | } from 'react-query'; 9 | 10 | import NavBar from '../../components/layouts/navbar.jsx' 11 | 12 | const SignIn = () => { 13 | const navigate = useNavigate(); 14 | const { register, handleSubmit, watch, formState: { errors } } = useForm(); 15 | const queryClient = useQueryClient(); 16 | const mutation = useMutation(getAuthToken, { 17 | onSuccess: (data) => { 18 | queryClient.setQueryData(['accessToken'], data.access_token); 19 | if (data.error === undefined) navigate('/'); 20 | } 21 | }); 22 | 23 | return ( 24 |
25 |
Sign In
26 |
27 |
28 |
Email
29 | 30 | {errors.email && This field is required} 31 |
32 |
33 |
Password
34 | 35 | {errors.password && This field is required} 36 |
37 | 38 | {mutation.status === 'success' ? Login failed : ''} 39 |
40 |
41 | ) 42 | } 43 | 44 | const getAuthToken = async (data) => { 45 | const formData = new FormData(); 46 | formData.append('grant_type', 'password'); 47 | formData.append('email', data.email); 48 | formData.append('password', data.password); 49 | 50 | return await fetch('/oauth/token', { 51 | method: 'POST', 52 | body: formData 53 | }).then(data => data.json()); 54 | }; 55 | 56 | export default SignIn; 57 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 12 | config.cache_classes = true 13 | 14 | # Eager loading loads your whole application. When running a single test locally, 15 | # this probably isn't necessary. It's a good idea to do in a continuous integration 16 | # system, or in some way before deploying your code. 17 | config.eager_load = ENV["CI"].present? 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Store uploaded files on the local file system in a temporary directory. 37 | config.active_storage.service = :test 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Tell Action Mailer not to deliver emails to the real world. 42 | # The :test delivery method accumulates sent emails in the 43 | # ActionMailer::Base.deliveries array. 44 | config.action_mailer.delivery_method = :test 45 | 46 | # Print deprecation notices to the stderr. 47 | config.active_support.deprecation = :stderr 48 | 49 | # Raise exceptions for disallowed deprecations. 50 | config.active_support.disallowed_deprecation = :raise 51 | 52 | # Tell Active Support which deprecation messages to disallow. 53 | config.active_support.disallowed_deprecation_warnings = [] 54 | 55 | # Raises error for missing translations. 56 | # config.i18n.raise_on_missing_translations = true 57 | 58 | # Annotate rendered view with file names. 59 | # config.action_view.annotate_rendered_view_with_filenames = true 60 | end 61 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 29 | } 30 | else 31 | config.action_controller.perform_caching = false 32 | 33 | config.cache_store = :null_store 34 | end 35 | 36 | # Store uploaded files on the local file system (see config/storage.yml for options). 37 | config.active_storage.service = :local 38 | 39 | # Don't care if the mailer can't send. 40 | config.action_mailer.raise_delivery_errors = false 41 | 42 | config.action_mailer.perform_caching = false 43 | 44 | # Print deprecation notices to the Rails logger. 45 | config.active_support.deprecation = :log 46 | 47 | # Raise exceptions for disallowed deprecations. 48 | config.active_support.disallowed_deprecation = :raise 49 | 50 | # Tell Active Support which deprecation messages to disallow. 51 | config.active_support.disallowed_deprecation_warnings = [] 52 | 53 | # Raise an error on page load if there are pending migrations. 54 | config.active_record.migration_error = :page_load 55 | 56 | # Highlight code that triggered database queries in logs. 57 | config.active_record.verbose_query_logs = true 58 | 59 | # Suppress logger output for asset requests. 60 | config.assets.quiet = true 61 | 62 | # Raises error for missing translations. 63 | # config.i18n.raise_on_missing_translations = true 64 | 65 | # Annotate rendered view with file names. 66 | # config.action_view.annotate_rendered_view_with_filenames = true 67 | 68 | # Uncomment if you wish to allow Action Cable access from any origin. 69 | # config.action_cable.disable_request_forgery_protection = true 70 | end 71 | -------------------------------------------------------------------------------- /app/javascript/components/favorites/favorite_button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { useMutation, useQueryClient } from 'react-query'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const FavoriteButton = (props) => { 7 | const [isFavorite, setIsFavorite] = useState(props.isFavorite); 8 | const queryClient = useQueryClient(); 9 | const accessToken = queryClient.getQueryData('accessToken'); 10 | 11 | const createFavorite = async () => { 12 | return await fetch('api/v1/favorites', { 13 | method: 'POST', 14 | headers: { 15 | 'Authorization': accessToken, 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify({'property_id': props.propertyId}) 19 | }).then(data => data.json()) 20 | .then(data => data.data.favorite_property_ids); 21 | }; 22 | 23 | const deleteFavorite = async () => { 24 | return await fetch('api/v1/favorites', { 25 | method: 'DELETE', 26 | headers: { 27 | 'Authorization': accessToken, 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify({'property_id': props.propertyId}) 31 | }).then(data => data.json()) 32 | .then(data => data.data.favorite_property_ids); 33 | }; 34 | 35 | const handleAddFavorite = useMutation(createFavorite, { 36 | onSuccess: (data) => { 37 | queryClient.setQueryData('favoriteIds', data); 38 | setIsFavorite(true); 39 | } 40 | }); 41 | 42 | const handleRemoveFavorite = useMutation(deleteFavorite,{ 43 | onSuccess: (data) => { 44 | queryClient.setQueryData('favoriteIds', data); 45 | setIsFavorite(false); 46 | } 47 | }); 48 | 49 | if (accessToken === undefined) return ( 50 | 53 | Add To Favorite 54 | 55 | ) 56 | 57 | if (isFavorite) { 58 | return ( 59 |
60 | 65 |
66 | ) 67 | } else { 68 | return ( 69 |
70 | 75 |
76 | ) 77 | } 78 | }; 79 | 80 | export default FavoriteButton; 81 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | ruby '3.0.2' 7 | 8 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 9 | gem 'rails', '~> 7.0.3' 10 | 11 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 12 | gem 'sprockets-rails' 13 | 14 | # Use the Puma web server [https://github.com/puma/puma] 15 | gem 'puma', '~> 5.0' 16 | 17 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 18 | # gem "importmap-rails" 19 | 20 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 21 | gem 'jbuilder' 22 | 23 | # Use Redis adapter to run Action Cable in production 24 | gem 'redis', '~> 4.0' 25 | 26 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 27 | # gem "kredis" 28 | 29 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 30 | gem 'bcrypt', '~> 3.1.7' 31 | 32 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 33 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 34 | 35 | # Reduces boot times through caching; required in config/boot.rb 36 | gem 'bootsnap', require: false 37 | 38 | # Use Sass to process CSS 39 | gem 'sassc-rails' 40 | 41 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 42 | # gem "image_processing", "~> 1.2" 43 | 44 | # Member System 45 | gem 'devise', '4.8.1' 46 | 47 | # Database PostgreSQL 48 | gem 'pg', '1.3.5' 49 | 50 | # Javascript Bundling 51 | gem 'jsbundling-rails', '~> 1.0' 52 | 53 | # CSS Bundling 54 | gem 'cssbundling-rails', '~> 1.1' 55 | 56 | # Administration 57 | gem 'cancancan', '3.3' 58 | gem 'rails_admin', '3.0' 59 | 60 | # Grape AP 61 | gem 'grape', '~> 1.6' 62 | 63 | # Model Searching 64 | gem 'ransack', '~> 3.2' 65 | 66 | # Redis Rails 67 | gem 'redis-rails', '~> 5.0' 68 | 69 | # Authorization 70 | gem 'doorkeeper', '~> 5.5' 71 | 72 | group :development, :test do 73 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 74 | gem 'debug', platforms: %i[mri mingw x64_mingw] 75 | %w[rspec-core rspec-expectations rspec-mocks rspec-rails rspec-support].each do |lib| 76 | gem lib, git: "https://github.com/rspec/#{lib}.git", branch: 'main' 77 | end 78 | gem 'factory_bot_rails' 79 | gem 'faker', git: 'https://github.com/faker-ruby/faker.git', branch: 'master' 80 | end 81 | 82 | group :development do 83 | # Use console on exceptions pages [https://github.com/rails/web-console] 84 | gem 'web-console' 85 | 86 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 87 | # gem "rack-mini-profiler" 88 | 89 | gem 'rubocop', '~> 1.30', require: false 90 | end 91 | 92 | group :test do 93 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 94 | gem 'capybara' 95 | gem 'selenium-webdriver' 96 | gem 'webdrivers' 97 | end 98 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2022_06_26_144233) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "plpgsql" 16 | 17 | create_table "favorites", force: :cascade do |t| 18 | t.integer "user_id" 19 | t.integer "property_id" 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | t.index ["property_id"], name: "index_favorites_on_property_id" 23 | t.index ["user_id"], name: "index_favorites_on_user_id" 24 | end 25 | 26 | create_table "oauth_access_tokens", force: :cascade do |t| 27 | t.bigint "resource_owner_id" 28 | t.string "token", null: false 29 | t.integer "application_id" 30 | t.string "refresh_token" 31 | t.integer "expires_in" 32 | t.datetime "revoked_at" 33 | t.datetime "created_at", null: false 34 | t.string "scopes" 35 | t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true 36 | t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" 37 | t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true 38 | end 39 | 40 | create_table "properties", force: :cascade do |t| 41 | t.integer "user_id" 42 | t.string "title" 43 | t.integer "amount_in_cent", null: false 44 | t.string "address_district" 45 | t.string "address_city" 46 | t.string "address_line" 47 | t.string "mrt_line" 48 | t.integer "room" 49 | t.string "image" 50 | t.datetime "created_at", null: false 51 | t.datetime "updated_at", null: false 52 | t.index ["address_district"], name: "index_properties_on_address_district" 53 | t.index ["title", "address_line"], name: "index_properties_on_title_and_address_line", unique: true 54 | end 55 | 56 | create_table "users", force: :cascade do |t| 57 | t.string "email", default: "", null: false 58 | t.string "encrypted_password", default: "", null: false 59 | t.string "reset_password_token" 60 | t.datetime "reset_password_sent_at" 61 | t.datetime "remember_created_at" 62 | t.datetime "created_at", null: false 63 | t.datetime "updated_at", null: false 64 | t.string "type" 65 | t.index ["email"], name: "index_users_on_email", unique: true 66 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is copied to spec/ when you run 'rails generate rspec:install' 4 | require 'spec_helper' 5 | ENV['RAILS_ENV'] ||= 'test' 6 | require_relative '../config/environment' 7 | # Prevent database truncation if the environment is production 8 | abort('The Rails environment is running in production mode!') if Rails.env.production? 9 | require 'rspec/rails' 10 | 11 | require 'support/factory_bot' 12 | # Add additional requires below this line. Rails is not loaded until this point! 13 | 14 | # Requires supporting ruby files with custom matchers and macros, etc, in 15 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 16 | # run as spec files by default. This means that files in spec/support that end 17 | # in _spec.rb will both be required and run as specs, causing the specs to be 18 | # run twice. It is recommended that you do not name files matching this glob to 19 | # end with _spec.rb. You can configure this pattern with the --pattern 20 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 21 | # 22 | # The following line is provided for convenience purposes. It has the downside 23 | # of increasing the boot-up time by auto-requiring all files in the support 24 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 25 | # require only the support files necessary. 26 | # 27 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } 28 | 29 | # Checks for pending migrations and applies them before tests are run. 30 | # If you are not using ActiveRecord, you can remove these lines. 31 | begin 32 | ActiveRecord::Migration.maintain_test_schema! 33 | rescue ActiveRecord::PendingMigrationError => e 34 | abort e.to_s.strip 35 | end 36 | RSpec.configure do |config| 37 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 38 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 39 | 40 | # Request spec for api folder 41 | config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: %r{/spec/api/} 42 | 43 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 44 | # examples within a transaction, remove the following line or assign false 45 | # instead of true. 46 | config.use_transactional_fixtures = true 47 | 48 | # You can uncomment this line to turn off ActiveRecord support entirely. 49 | # config.use_active_record = false 50 | 51 | # RSpec Rails can automatically mix in different behaviours to your tests 52 | # based on their file location, for example enabling you to call `get` and 53 | # `post` in specs under `spec/controllers`. 54 | # 55 | # You can disable this behaviour by removing the line below, and instead 56 | # explicitly tag your specs with their type, e.g.: 57 | # 58 | # RSpec.describe UsersController, type: :controller do 59 | # # ... 60 | # end 61 | # 62 | # The different available types are documented in the features, such as in 63 | # https://relishapp.com/rspec/rspec-rails/docs 64 | config.infer_spec_type_from_file_location! 65 | 66 | # Filter lines from Rails gems in backtraces. 67 | config.filter_rails_from_backtrace! 68 | # arbitrary gems may also be filtered via: 69 | # config.filter_gems_from_backtrace("gem name") 70 | end 71 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /spec/api/v1/favorites_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.describe V1::Favorites do 6 | let(:current_user) { create(:user_with_favorites) } 7 | let(:access_token) do 8 | Doorkeeper::AccessToken.create(resource_owner_id: current_user.id).token 9 | end 10 | 11 | describe 'GET /api/v1/favorites' do 12 | describe 'Has Authentication' do 13 | it 'responds 401 without authorization token' do 14 | get('/api/v1/favorites') 15 | expect(response.status).to eq(401) 16 | end 17 | 18 | it 'responds 200 with authorization token' do 19 | get '/api/v1/favorites', headers: { Authorization: access_token } 20 | expect(response.status).to eq(200) 21 | end 22 | end 23 | 24 | describe 'Response includes data' do 25 | subject { get '/api/v1/favorites', headers: { Authorization: access_token } } 26 | before { subject } 27 | 28 | it 'responds current user favorite properties amount' do 29 | expect(JSON.parse(response.body)['data']['items'].count).to eq(3) 30 | end 31 | 32 | it 'responds current user favorite properties content' do 33 | expect(JSON.parse(response.body)['data']['items'].map { |item| item['id'] }) 34 | .to eq(current_user.favorite_properties.pluck(:id)) 35 | end 36 | end 37 | end 38 | 39 | describe 'POST /api/v1/favorites' do 40 | describe 'Has Authentication' do 41 | it 'responds 401 without authorization token' do 42 | post '/api/v1/favorites' 43 | expect(response.status).to eq(401) 44 | end 45 | 46 | it 'responds 201 with authorization token' do 47 | post '/api/v1/favorites', headers: { Authorization: access_token } 48 | expect(response.status).to eq(201) 49 | end 50 | end 51 | 52 | describe 'Response data' do 53 | let(:property) { create(:property) } 54 | 55 | context 'Adding a property' do 56 | it 'responds the latest current user favorite_property_ids after success' do 57 | post '/api/v1/favorites', headers: { Authorization: access_token }, params: { property_id: property.id } 58 | expect(JSON.parse(response.body)['data']['favorite_property_ids']) 59 | .to eq(current_user.favorite_properties.pluck(:id)) 60 | end 61 | 62 | it 'after success the amount of favorites should be 4' do 63 | post '/api/v1/favorites', headers: { Authorization: access_token }, params: { property_id: property.id } 64 | expect(JSON.parse(response.body)['data']['favorite_property_ids'].count).to eq(4) 65 | end 66 | end 67 | end 68 | end 69 | 70 | describe 'DELETE /api/v1/favorites' do 71 | describe 'Has Authentication' do 72 | it 'responds 401 without authorization token' do 73 | delete '/api/v1/favorites' 74 | expect(response.status).to eq(401) 75 | end 76 | 77 | it 'responds 200 with authorization token' do 78 | delete '/api/v1/favorites', headers: { Authorization: access_token } 79 | expect(response.status).to eq(200) 80 | end 81 | end 82 | 83 | describe 'Delete a favorite' do 84 | it 'after delete success the amount of favorites should be 2' do 85 | delete '/api/v1/favorites', headers: { Authorization: access_token }, 86 | params: { property_id: current_user.favorite_properties.first.id } 87 | expect(JSON.parse(response.body)['data']['favorite_property_ids'].count).to eq(2) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /app/scrapers/house_scraper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'net/http' 4 | 5 | class HouseScraper 6 | attr_reader :response_data, :parsed_properties 7 | 8 | def initialize 9 | @parsed_properties = [] 10 | @target_cities = { 11 | taipei: 'https://www.urhouse.com.tw/en/rentals/ajax?page=1&filter=JTdCJTIydHlwZSUyMiUzQSUyMnJlc2lkZW50aWFsJTIyJTJDJTIyY2l0eSUyMiUzQSUyMlRhaXBlaSUyMENpdHklMjIlMkMlMjJkaXN0JTIyJTNBJTVCJTVEJTJDJTIybGF5b3V0JTIyJTNBJTIyJTIyJTJDJTIycmVudCUyMiUzQSU3QiUyMm1pbiUyMiUzQSUyMjMwMDAwJTIyJTJDJTIybWF4JTIyJTNBJTIyMTAwMDAwJTIyJTdEJTJDJTIyZmxvb3Jfc2l6ZSUyMiUzQSU3QiUyMm1pbiUyMiUzQSUyMiUyMiUyQyUyMm1heCUyMiUzQSUyMiUyMiU3RCUyQyUyMnBhcmtpbmclMjIlM0ElN0IlMjJwbGFuZSUyMiUzQSUyMiUyMiUyQyUyMm1lY2hhbmljYWwlMjIlM0ElMjIlMjIlN0QlMkMlMjJoYXNfcGFya2luZyUyMiUzQSUyMiUyMiUyQyUyMm1hcCUyMiUzQSU3QiUyMnNvdXRoJTIyJTNBMCUyQyUyMndlc3QlMjIlM0EwJTJDJTIybm9ydGglMjIlM0EwJTJDJTIyZWFzdCUyMiUzQTAlN0QlMkMlMjJyZXNpZGVudGlhbCUyMiUzQSU3QiUyMnRvdGFsX3Jvb20lMjIlM0ElN0IlMjJtaW4lMjIlM0ElMjIlMjIlMkMlMjJtYXglMjIlM0ElMjIlMjIlN0QlN0QlMkMlMjJvZmZpY2UlMjIlM0ElN0IlN0QlMkMlMjJzdG9yZWZyb250JTIyJTNBJTdCJTdEJTdE&ordering=price&direction=ASC&mode=list', 12 | new_taipei: 'https://www.urhouse.com.tw/en/rentals/ajax?page=1&filter=JTdCJTIydHlwZSUyMiUzQSUyMnJlc2lkZW50aWFsJTIyJTJDJTIyY2l0eSUyMiUzQSUyMk5ldyUyMFRhaXBlaSUyMENpdHklMjIlMkMlMjJkaXN0JTIyJTNBJTVCJTVEJTJDJTIybGF5b3V0JTIyJTNBJTIyJTIyJTJDJTIycmVudCUyMiUzQSU3QiUyMm1pbiUyMiUzQSUyMjMwMDAwJTIyJTJDJTIybWF4JTIyJTNBJTIyMTAwMDAwJTIyJTdEJTJDJTIyZmxvb3Jfc2l6ZSUyMiUzQSU3QiUyMm1pbiUyMiUzQSUyMiUyMiUyQyUyMm1heCUyMiUzQSUyMiUyMiU3RCUyQyUyMnBhcmtpbmclMjIlM0ElN0IlMjJwbGFuZSUyMiUzQSUyMiUyMiUyQyUyMm1lY2hhbmljYWwlMjIlM0ElMjIlMjIlN0QlMkMlMjJoYXNfcGFya2luZyUyMiUzQSUyMiUyMiUyQyUyMm1hcCUyMiUzQSU3QiUyMnNvdXRoJTIyJTNBMCUyQyUyMndlc3QlMjIlM0EwJTJDJTIybm9ydGglMjIlM0EwJTJDJTIyZWFzdCUyMiUzQTAlN0QlMkMlMjJyZXNpZGVudGlhbCUyMiUzQSU3QiUyMnRvdGFsX3Jvb20lMjIlM0ElN0IlMjJtaW4lMjIlM0ElMjIlMjIlMkMlMjJtYXglMjIlM0ElMjIlMjIlN0QlN0QlMkMlMjJvZmZpY2UlMjIlM0ElN0IlN0QlMkMlMjJzdG9yZWZyb250JTIyJTNBJTdCJTdEJTdE&ordering=price&direction=ASC&mode=list' 13 | } 14 | end 15 | 16 | def crawl! 17 | @target_cities.each do |city, url| 18 | Rails.logger.info("*** Start scraping #{city} data ***") 19 | @page = 1 20 | fetch_data(current_page_url(url)) 21 | parse_data 22 | save_parsed_properties 23 | fetch_remaining_pages(current_page_url(url)) 24 | end 25 | 26 | true 27 | end 28 | 29 | private 30 | 31 | def current_page_url(url) 32 | url.sub('page=1', "page=#{@page}") 33 | end 34 | 35 | def fetch_data(target_url) 36 | Rails.logger.info("*** Start to fetch data from url: #{target_url} ***") 37 | response = Net::HTTP.get_response(URI(target_url)) 38 | @response_data = JSON.parse(response.body)['data'] 39 | @total_pages = @response_data['pagination'].keys.last.to_i 40 | Rails.logger.info("*** Fetching data is success total pages: #{@total_pages} ***") 41 | end 42 | 43 | def parse_data 44 | sample_properties = @response_data['items'].sample(6) 45 | @parsed_properties = sample_properties.map do |item| 46 | parse_property(item) 47 | end 48 | end 49 | 50 | def parse_property(item) 51 | { 52 | title: item['title'], 53 | amount_in_cent: item['rent'].to_i / 30 * 100, 54 | address_district: item['dist'], 55 | address_city: item['city'], 56 | address_line: item['road_address'], 57 | room: item['total_room'], 58 | mrt_line: item['mrt_line'], 59 | image: item['image_url'] 60 | } 61 | end 62 | 63 | def fetch_remaining_pages(url) 64 | return if @total_pages == 1 65 | 66 | (2..@total_pages).to_a.each do |page| 67 | @page = page 68 | fetch_data(current_page_url(url)) 69 | parse_data 70 | save_parsed_properties 71 | end 72 | end 73 | 74 | def save_parsed_properties 75 | @parsed_properties.each do |property| 76 | Property.upsert(property, unique_by: [:index_properties_on_title_and_address_line]) 77 | end 78 | Rails.logger.info("*** save data from page: #{@page} is successful ***") 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = "http://assets.example.com" 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = "wss://example.com/cable" 46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "seedhouse_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Don't log any deprecations. 76 | config.active_support.report_deprecations = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | 81 | # Use a different logger for distributed setups. 82 | # require "syslog/logger" 83 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 84 | 85 | if ENV["RAILS_LOG_TO_STDOUT"].present? 86 | logger = ActiveSupport::Logger.new(STDOUT) 87 | logger.formatter = config.log_formatter 88 | config.logger = ActiveSupport::TaggedLogging.new(logger) 89 | end 90 | 91 | # Do not dump schema after migrations. 92 | config.active_record.dump_schema_after_migration = false 93 | end 94 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/heartcombo/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | email_changed: 27 | subject: "Email Changed" 28 | password_change: 29 | subject: "Password Changed" 30 | omniauth_callbacks: 31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 32 | success: "Successfully authenticated from %{kind} account." 33 | passwords: 34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 36 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 37 | updated: "Your password has been changed successfully. You are now signed in." 38 | updated_not_active: "Your password has been changed successfully." 39 | registrations: 40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 41 | signed_up: "Welcome! You have signed up successfully." 42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 44 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." 46 | updated: "Your account has been updated successfully." 47 | updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." 48 | sessions: 49 | signed_in: "Signed in successfully." 50 | signed_out: "Signed out successfully." 51 | already_signed_out: "Signed out successfully." 52 | unlocks: 53 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 54 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 55 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 56 | errors: 57 | messages: 58 | already_confirmed: "was already confirmed, please try signing in" 59 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 60 | expired: "has expired, please request a new one" 61 | not_found: "not found" 62 | not_locked: "was not locked" 63 | not_saved: 64 | one: "1 error prohibited this %{resource} from being saved:" 65 | other: "%{count} errors prohibited this %{resource} from being saved:" 66 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 18 | RSpec.configure do |config| 19 | # rspec-expectations config goes here. You can use an alternate 20 | # assertion/expectation library such as wrong or the stdlib/minitest 21 | # assertions if you prefer. 22 | config.expect_with :rspec do |expectations| 23 | # This option will default to `true` in RSpec 4. It makes the `description` 24 | # and `failure_message` of custom matchers include text for helper methods 25 | # defined using `chain`, e.g.: 26 | # be_bigger_than(2).and_smaller_than(4).description 27 | # # => "be bigger than 2 and smaller than 4" 28 | # ...rather than: 29 | # # => "be bigger than 2" 30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 31 | end 32 | 33 | # rspec-mocks config goes here. You can use an alternate test double 34 | # library (such as bogus or mocha) by changing the `mock_with` option here. 35 | config.mock_with :rspec do |mocks| 36 | # Prevents you from mocking or stubbing a method that does not exist on 37 | # a real object. This is generally recommended, and will default to 38 | # `true` in RSpec 4. 39 | mocks.verify_partial_doubles = true 40 | end 41 | 42 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 43 | # have no way to turn it off -- the option exists only for backwards 44 | # compatibility in RSpec 3). It causes shared context metadata to be 45 | # inherited by the metadata hash of host groups and examples, rather than 46 | # triggering implicit auto-inclusion in groups with matching metadata. 47 | config.shared_context_metadata_behavior = :apply_to_host_groups 48 | 49 | # The settings below are suggested to provide a good initial experience 50 | # with RSpec, but feel free to customize to your heart's content. 51 | # # This allows you to limit a spec run to individual examples or groups 52 | # # you care about by tagging them with `:focus` metadata. When nothing 53 | # # is tagged with `:focus`, all examples get run. RSpec also provides 54 | # # aliases for `it`, `describe`, and `context` that include `:focus` 55 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 56 | # config.filter_run_when_matching :focus 57 | # 58 | # # Allows RSpec to persist some state between runs in order to support 59 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 60 | # # you configure your source control system to ignore this file. 61 | # config.example_status_persistence_file_path = "spec/examples.txt" 62 | # 63 | # # Limits the available syntax to the non-monkey patched syntax that is 64 | # # recommended. For more details, see: 65 | # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode 66 | # config.disable_monkey_patching! 67 | # 68 | # # Many RSpec users commonly either run the entire suite or an individual 69 | # # file, and it's useful to allow more verbose output when running an 70 | # # individual spec file. 71 | # if config.files_to_run.one? 72 | # # Use the documentation formatter for detailed output, 73 | # # unless a formatter has already been configured 74 | # # (e.g. via a command-line flag). 75 | # config.default_formatter = "doc" 76 | # end 77 | # 78 | # # Print the 10 slowest examples and example groups at the 79 | # # end of the spec run, to help surface which specs are running 80 | # # particularly slow. 81 | # config.profile_examples = 10 82 | # 83 | # # Run specs in random order to surface order dependencies. If you find an 84 | # # order dependency and want to debug it, you can fix the order by providing 85 | # # the seed, which is printed after each run. 86 | # # --seed 1234 87 | # config.order = :random 88 | # 89 | # # Seed global randomization in this process using the `--seed` CLI option. 90 | # # Setting this allows you to use `--seed` to deterministically reproduce 91 | # # test failures related to randomization by passing the same `--seed` value 92 | # # as the one that triggered the failure. 93 | # Kernel.srand config.seed 94 | end 95 | -------------------------------------------------------------------------------- /config/locales/doorkeeper.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | attributes: 4 | doorkeeper/application: 5 | name: 'Name' 6 | redirect_uri: 'Redirect URI' 7 | errors: 8 | models: 9 | doorkeeper/application: 10 | attributes: 11 | redirect_uri: 12 | fragment_present: 'cannot contain a fragment.' 13 | invalid_uri: 'must be a valid URI.' 14 | unspecified_scheme: 'must specify a scheme.' 15 | relative_uri: 'must be an absolute URI.' 16 | secured_uri: 'must be an HTTPS/SSL URI.' 17 | forbidden_uri: 'is forbidden by the server.' 18 | scopes: 19 | not_match_configured: "doesn't match configured on the server." 20 | 21 | doorkeeper: 22 | applications: 23 | confirmations: 24 | destroy: 'Are you sure?' 25 | buttons: 26 | edit: 'Edit' 27 | destroy: 'Destroy' 28 | submit: 'Submit' 29 | cancel: 'Cancel' 30 | authorize: 'Authorize' 31 | form: 32 | error: 'Whoops! Check your form for possible errors' 33 | help: 34 | confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' 35 | redirect_uri: 'Use one line per URI' 36 | blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." 37 | scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' 38 | edit: 39 | title: 'Edit application' 40 | index: 41 | title: 'Your applications' 42 | new: 'New Application' 43 | name: 'Name' 44 | callback_url: 'Callback URL' 45 | confidential: 'Confidential?' 46 | actions: 'Actions' 47 | confidentiality: 48 | 'yes': 'Yes' 49 | 'no': 'No' 50 | new: 51 | title: 'New Application' 52 | show: 53 | title: 'Application: %{name}' 54 | application_id: 'UID' 55 | secret: 'Secret' 56 | secret_hashed: 'Secret hashed' 57 | scopes: 'Scopes' 58 | confidential: 'Confidential' 59 | callback_urls: 'Callback urls' 60 | actions: 'Actions' 61 | not_defined: 'Not defined' 62 | 63 | authorizations: 64 | buttons: 65 | authorize: 'Authorize' 66 | deny: 'Deny' 67 | error: 68 | title: 'An error has occurred' 69 | new: 70 | title: 'Authorization required' 71 | prompt: 'Authorize %{client_name} to use your account?' 72 | able_to: 'This application will be able to' 73 | show: 74 | title: 'Authorization code' 75 | form_post: 76 | title: 'Submit this form' 77 | 78 | authorized_applications: 79 | confirmations: 80 | revoke: 'Are you sure?' 81 | buttons: 82 | revoke: 'Revoke' 83 | index: 84 | title: 'Your authorized applications' 85 | application: 'Application' 86 | created_at: 'Created At' 87 | date_format: '%Y-%m-%d %H:%M:%S' 88 | 89 | pre_authorization: 90 | status: 'Pre-authorization' 91 | 92 | errors: 93 | messages: 94 | # Common error messages 95 | invalid_request: 96 | unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' 97 | missing_param: 'Missing required parameter: %{value}.' 98 | request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' 99 | invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." 100 | unauthorized_client: 'The client is not authorized to perform this request using this method.' 101 | access_denied: 'The resource owner or authorization server denied the request.' 102 | invalid_scope: 'The requested scope is invalid, unknown, or malformed.' 103 | invalid_code_challenge_method: 'The code challenge method must be plain or S256.' 104 | server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' 105 | temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' 106 | 107 | # Configuration error messages 108 | credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' 109 | resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' 110 | admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' 111 | 112 | # Access grant errors 113 | unsupported_response_type: 'The authorization server does not support this response type.' 114 | unsupported_response_mode: 'The authorization server does not support this response mode.' 115 | 116 | # Access token errors 117 | invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' 118 | invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' 119 | unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' 120 | 121 | invalid_token: 122 | revoked: "The access token was revoked" 123 | expired: "The access token expired" 124 | unknown: "The access token is invalid" 125 | revoke: 126 | unauthorized: "You are not authorized to revoke this token" 127 | 128 | forbidden_token: 129 | missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".' 130 | 131 | flash: 132 | applications: 133 | create: 134 | notice: 'Application created.' 135 | destroy: 136 | notice: 'Application deleted.' 137 | update: 138 | notice: 'Application updated.' 139 | authorized_applications: 140 | destroy: 141 | notice: 'Application revoked.' 142 | 143 | layouts: 144 | admin: 145 | title: 'Doorkeeper' 146 | nav: 147 | oauth2_provider: 'OAuth2 Provider' 148 | applications: 'Applications' 149 | home: 'Home' 150 | application: 151 | title: 'OAuth authorization required' 152 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/rt/9jl307_j0mz499jsbbyq9fm80000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // The default configuration for fake timers 54 | // fakeTimers: { 55 | // "enableGlobally": false 56 | // }, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: undefined, 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: undefined, 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "50%", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "mjs", 82 | // "cjs", 83 | // "jsx", 84 | // "ts", 85 | // "tsx", 86 | // "json", 87 | // "node" 88 | // ], 89 | 90 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 91 | // moduleNameMapper: {}, 92 | 93 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 94 | // modulePathIgnorePatterns: [], 95 | 96 | // Activates notifications for test results 97 | // notify: false, 98 | 99 | // An enum that specifies notification mode. Requires { notify: true } 100 | // notifyMode: "failure-change", 101 | 102 | // A preset that is used as a base for Jest's configuration 103 | // preset: undefined, 104 | 105 | // Run tests from one or more projects 106 | // projects: undefined, 107 | 108 | // Use this configuration option to add custom reporters to Jest 109 | // reporters: undefined, 110 | 111 | // Automatically reset mock state before every test 112 | // resetMocks: false, 113 | 114 | // Reset the module registry before running each individual test 115 | // resetModules: false, 116 | 117 | // A path to a custom resolver 118 | // resolver: undefined, 119 | 120 | // Automatically restore mock state and implementation before every test 121 | // restoreMocks: false, 122 | 123 | // The root directory that Jest should scan for tests and modules within 124 | // rootDir: undefined, 125 | 126 | // A list of paths to directories that Jest should use to search for files in 127 | // roots: [ 128 | // "" 129 | // ], 130 | 131 | // Allows you to use a custom runner instead of Jest's default test runner 132 | // runner: "jest-runner", 133 | 134 | // The paths to modules that run some code to configure or set up the testing environment before each test 135 | // setupFiles: [], 136 | 137 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 138 | // setupFilesAfterEnv: [], 139 | 140 | // The number of seconds after which a test is considered as slow and reported as such in the results. 141 | // slowTestThreshold: 5, 142 | 143 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 144 | // snapshotSerializers: [], 145 | 146 | // The test environment that will be used for testing 147 | testEnvironment: "jsdom", 148 | 149 | // Options that will be passed to the testEnvironment 150 | // testEnvironmentOptions: {}, 151 | 152 | // Adds a location field to test results 153 | // testLocationInResults: false, 154 | 155 | // The glob patterns Jest uses to detect test files 156 | // testMatch: [ 157 | // "**/__tests__/**/*.[jt]s?(x)", 158 | // "**/?(*.)+(spec|test).[tj]s?(x)" 159 | // ], 160 | 161 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 162 | // testPathIgnorePatterns: [ 163 | // "/node_modules/" 164 | // ], 165 | 166 | // The regexp pattern or array of patterns that Jest uses to detect test files 167 | // testRegex: [], 168 | 169 | // This option allows the use of a custom results processor 170 | // testResultsProcessor: undefined, 171 | 172 | // This option allows use of a custom test runner 173 | // testRunner: "jest-circus/runner", 174 | 175 | // A map from regular expressions to paths to transformers 176 | 177 | transform: { 178 | "\\.[jt]sx?$": [ 179 | "esbuild-jest", 180 | { 181 | sourcemap: true, 182 | loaders: { 183 | '.test.js': 'jsx', 184 | '.js': 'jsx', 185 | } 186 | } 187 | ] 188 | } 189 | // transform: { 190 | // "\\.[jt]sx?$": [ 'esbuild-jest', { 191 | // loaders: { 192 | // '.test.js': 'jsx', 193 | // '.js': 'jsx' 194 | // } 195 | // } 196 | // ] 197 | // }, 198 | 199 | 200 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 201 | // transformIgnorePatterns: [ 202 | // "/node_modules/", 203 | // "\\.pnp\\.[^\\/]+$" 204 | // ], 205 | 206 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 207 | // unmockedModulePathPatterns: undefined, 208 | 209 | // Indicates whether each individual test should be reported during the run 210 | // verbose: undefined, 211 | 212 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 213 | // watchPathIgnorePatterns: [], 214 | 215 | // Whether to use watchman for file crawling 216 | // watchman: true, 217 | }; 218 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/faker-ruby/faker.git 3 | revision: 85016fe969c0ea6044def6192c169a9992747742 4 | branch: master 5 | specs: 6 | faker (2.21.0) 7 | i18n (>= 1.8.11, < 2) 8 | 9 | GIT 10 | remote: https://github.com/rspec/rspec-core.git 11 | revision: 814c1c1ecf79d2191718b2795b6d12d9d522f60b 12 | branch: main 13 | specs: 14 | rspec-core (3.12.0.pre) 15 | rspec-support (= 3.12.0.pre) 16 | 17 | GIT 18 | remote: https://github.com/rspec/rspec-expectations.git 19 | revision: 1bc30dc41665ade783340d71e1d8f30bf3c9b995 20 | branch: main 21 | specs: 22 | rspec-expectations (3.12.0.pre) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (= 3.12.0.pre) 25 | 26 | GIT 27 | remote: https://github.com/rspec/rspec-mocks.git 28 | revision: eb6d7e93474308d913fe794bebdee7d50fb33e28 29 | branch: main 30 | specs: 31 | rspec-mocks (3.12.0.pre) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (= 3.12.0.pre) 34 | 35 | GIT 36 | remote: https://github.com/rspec/rspec-rails.git 37 | revision: edb30624f0a1c8dcf37da90780a22d8c07e0cd31 38 | branch: main 39 | specs: 40 | rspec-rails (6.0.0.pre) 41 | actionpack (>= 6.1) 42 | activesupport (>= 6.1) 43 | railties (>= 6.1) 44 | rspec-core (= 3.12.0.pre) 45 | rspec-expectations (= 3.12.0.pre) 46 | rspec-mocks (= 3.12.0.pre) 47 | rspec-support (= 3.12.0.pre) 48 | 49 | GIT 50 | remote: https://github.com/rspec/rspec-support.git 51 | revision: 528d88ce6fac5f83390bf430d1c47608e9d8d29a 52 | branch: main 53 | specs: 54 | rspec-support (3.12.0.pre) 55 | 56 | GEM 57 | remote: https://rubygems.org/ 58 | specs: 59 | actioncable (7.0.3) 60 | actionpack (= 7.0.3) 61 | activesupport (= 7.0.3) 62 | nio4r (~> 2.0) 63 | websocket-driver (>= 0.6.1) 64 | actionmailbox (7.0.3) 65 | actionpack (= 7.0.3) 66 | activejob (= 7.0.3) 67 | activerecord (= 7.0.3) 68 | activestorage (= 7.0.3) 69 | activesupport (= 7.0.3) 70 | mail (>= 2.7.1) 71 | net-imap 72 | net-pop 73 | net-smtp 74 | actionmailer (7.0.3) 75 | actionpack (= 7.0.3) 76 | actionview (= 7.0.3) 77 | activejob (= 7.0.3) 78 | activesupport (= 7.0.3) 79 | mail (~> 2.5, >= 2.5.4) 80 | net-imap 81 | net-pop 82 | net-smtp 83 | rails-dom-testing (~> 2.0) 84 | actionpack (7.0.3) 85 | actionview (= 7.0.3) 86 | activesupport (= 7.0.3) 87 | rack (~> 2.0, >= 2.2.0) 88 | rack-test (>= 0.6.3) 89 | rails-dom-testing (~> 2.0) 90 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 91 | actiontext (7.0.3) 92 | actionpack (= 7.0.3) 93 | activerecord (= 7.0.3) 94 | activestorage (= 7.0.3) 95 | activesupport (= 7.0.3) 96 | globalid (>= 0.6.0) 97 | nokogiri (>= 1.8.5) 98 | actionview (7.0.3) 99 | activesupport (= 7.0.3) 100 | builder (~> 3.1) 101 | erubi (~> 1.4) 102 | rails-dom-testing (~> 2.0) 103 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 104 | activejob (7.0.3) 105 | activesupport (= 7.0.3) 106 | globalid (>= 0.3.6) 107 | activemodel (7.0.3) 108 | activesupport (= 7.0.3) 109 | activemodel-serializers-xml (1.0.2) 110 | activemodel (> 5.x) 111 | activesupport (> 5.x) 112 | builder (~> 3.1) 113 | activerecord (7.0.3) 114 | activemodel (= 7.0.3) 115 | activesupport (= 7.0.3) 116 | activestorage (7.0.3) 117 | actionpack (= 7.0.3) 118 | activejob (= 7.0.3) 119 | activerecord (= 7.0.3) 120 | activesupport (= 7.0.3) 121 | marcel (~> 1.0) 122 | mini_mime (>= 1.1.0) 123 | activesupport (7.0.3) 124 | concurrent-ruby (~> 1.0, >= 1.0.2) 125 | i18n (>= 1.6, < 2) 126 | minitest (>= 5.1) 127 | tzinfo (~> 2.0) 128 | addressable (2.8.0) 129 | public_suffix (>= 2.0.2, < 5.0) 130 | ast (2.4.2) 131 | bcrypt (3.1.18) 132 | bindex (0.8.1) 133 | bootsnap (1.12.0) 134 | msgpack (~> 1.2) 135 | builder (3.2.4) 136 | cancancan (3.3.0) 137 | capybara (3.37.1) 138 | addressable 139 | matrix 140 | mini_mime (>= 0.1.3) 141 | nokogiri (~> 1.8) 142 | rack (>= 1.6.0) 143 | rack-test (>= 0.6.3) 144 | regexp_parser (>= 1.5, < 3.0) 145 | xpath (~> 3.2) 146 | childprocess (4.1.0) 147 | concurrent-ruby (1.1.10) 148 | crass (1.0.6) 149 | cssbundling-rails (1.1.0) 150 | railties (>= 6.0.0) 151 | debug (1.5.0) 152 | irb (>= 1.3.6) 153 | reline (>= 0.2.7) 154 | devise (4.8.1) 155 | bcrypt (~> 3.0) 156 | orm_adapter (~> 0.1) 157 | railties (>= 4.1.0) 158 | responders 159 | warden (~> 1.2.3) 160 | diff-lcs (1.5.0) 161 | digest (3.1.0) 162 | doorkeeper (5.5.4) 163 | railties (>= 5) 164 | dry-configurable (0.15.0) 165 | concurrent-ruby (~> 1.0) 166 | dry-core (~> 0.6) 167 | dry-container (0.9.0) 168 | concurrent-ruby (~> 1.0) 169 | dry-configurable (~> 0.13, >= 0.13.0) 170 | dry-core (0.7.1) 171 | concurrent-ruby (~> 1.0) 172 | dry-inflector (0.2.1) 173 | dry-logic (1.2.0) 174 | concurrent-ruby (~> 1.0) 175 | dry-core (~> 0.5, >= 0.5) 176 | dry-types (1.5.1) 177 | concurrent-ruby (~> 1.0) 178 | dry-container (~> 0.3) 179 | dry-core (~> 0.5, >= 0.5) 180 | dry-inflector (~> 0.1, >= 0.1.2) 181 | dry-logic (~> 1.0, >= 1.0.2) 182 | erubi (1.10.0) 183 | factory_bot (6.2.1) 184 | activesupport (>= 5.0.0) 185 | factory_bot_rails (6.2.0) 186 | factory_bot (~> 6.2.0) 187 | railties (>= 5.0.0) 188 | ffi (1.15.5) 189 | globalid (1.0.0) 190 | activesupport (>= 5.0) 191 | grape (1.6.2) 192 | activesupport 193 | builder 194 | dry-types (>= 1.1) 195 | mustermann-grape (~> 1.0.0) 196 | rack (>= 1.3.0) 197 | rack-accept 198 | i18n (1.10.0) 199 | concurrent-ruby (~> 1.0) 200 | io-console (0.5.11) 201 | irb (1.4.1) 202 | reline (>= 0.3.0) 203 | jbuilder (2.11.5) 204 | actionview (>= 5.0.0) 205 | activesupport (>= 5.0.0) 206 | jsbundling-rails (1.0.2) 207 | railties (>= 6.0.0) 208 | kaminari (1.2.2) 209 | activesupport (>= 4.1.0) 210 | kaminari-actionview (= 1.2.2) 211 | kaminari-activerecord (= 1.2.2) 212 | kaminari-core (= 1.2.2) 213 | kaminari-actionview (1.2.2) 214 | actionview 215 | kaminari-core (= 1.2.2) 216 | kaminari-activerecord (1.2.2) 217 | activerecord 218 | kaminari-core (= 1.2.2) 219 | kaminari-core (1.2.2) 220 | loofah (2.18.0) 221 | crass (~> 1.0.2) 222 | nokogiri (>= 1.5.9) 223 | mail (2.7.1) 224 | mini_mime (>= 0.1.1) 225 | marcel (1.0.2) 226 | matrix (0.4.2) 227 | method_source (1.0.0) 228 | mini_mime (1.1.2) 229 | minitest (5.15.0) 230 | msgpack (1.5.2) 231 | mustermann (1.1.1) 232 | ruby2_keywords (~> 0.0.1) 233 | mustermann-grape (1.0.2) 234 | mustermann (>= 1.0.0) 235 | nested_form (0.3.2) 236 | net-imap (0.2.3) 237 | digest 238 | net-protocol 239 | strscan 240 | net-pop (0.1.1) 241 | digest 242 | net-protocol 243 | timeout 244 | net-protocol (0.1.3) 245 | timeout 246 | net-smtp (0.3.1) 247 | digest 248 | net-protocol 249 | timeout 250 | nio4r (2.5.8) 251 | nokogiri (1.13.6-arm64-darwin) 252 | racc (~> 1.4) 253 | orm_adapter (0.5.0) 254 | parallel (1.22.1) 255 | parser (3.1.2.0) 256 | ast (~> 2.4.1) 257 | pg (1.3.5) 258 | public_suffix (4.0.7) 259 | puma (5.6.4) 260 | nio4r (~> 2.0) 261 | racc (1.6.0) 262 | rack (2.2.3.1) 263 | rack-accept (0.4.5) 264 | rack (>= 0.4) 265 | rack-test (1.1.0) 266 | rack (>= 1.0, < 3) 267 | rails (7.0.3) 268 | actioncable (= 7.0.3) 269 | actionmailbox (= 7.0.3) 270 | actionmailer (= 7.0.3) 271 | actionpack (= 7.0.3) 272 | actiontext (= 7.0.3) 273 | actionview (= 7.0.3) 274 | activejob (= 7.0.3) 275 | activemodel (= 7.0.3) 276 | activerecord (= 7.0.3) 277 | activestorage (= 7.0.3) 278 | activesupport (= 7.0.3) 279 | bundler (>= 1.15.0) 280 | railties (= 7.0.3) 281 | rails-dom-testing (2.0.3) 282 | activesupport (>= 4.2.0) 283 | nokogiri (>= 1.6) 284 | rails-html-sanitizer (1.4.3) 285 | loofah (~> 2.3) 286 | rails_admin (3.0.0) 287 | activemodel-serializers-xml (>= 1.0) 288 | kaminari (>= 0.14, < 2.0) 289 | nested_form (~> 0.3) 290 | rails (>= 6.0, < 8) 291 | turbo-rails (~> 1.0) 292 | railties (7.0.3) 293 | actionpack (= 7.0.3) 294 | activesupport (= 7.0.3) 295 | method_source 296 | rake (>= 12.2) 297 | thor (~> 1.0) 298 | zeitwerk (~> 2.5) 299 | rainbow (3.1.1) 300 | rake (13.0.6) 301 | ransack (3.2.1) 302 | activerecord (>= 6.1.5) 303 | activesupport (>= 6.1.5) 304 | i18n 305 | redis (4.6.0) 306 | redis-actionpack (5.3.0) 307 | actionpack (>= 5, < 8) 308 | redis-rack (>= 2.1.0, < 3) 309 | redis-store (>= 1.1.0, < 2) 310 | redis-activesupport (5.3.0) 311 | activesupport (>= 3, < 8) 312 | redis-store (>= 1.3, < 2) 313 | redis-rack (2.1.4) 314 | rack (>= 2.0.8, < 3) 315 | redis-store (>= 1.2, < 2) 316 | redis-rails (5.0.2) 317 | redis-actionpack (>= 5.0, < 6) 318 | redis-activesupport (>= 5.0, < 6) 319 | redis-store (>= 1.2, < 2) 320 | redis-store (1.9.1) 321 | redis (>= 4, < 5) 322 | regexp_parser (2.5.0) 323 | reline (0.3.1) 324 | io-console (~> 0.5) 325 | responders (3.0.1) 326 | actionpack (>= 5.0) 327 | railties (>= 5.0) 328 | rexml (3.2.5) 329 | rubocop (1.30.1) 330 | parallel (~> 1.10) 331 | parser (>= 3.1.0.0) 332 | rainbow (>= 2.2.2, < 4.0) 333 | regexp_parser (>= 1.8, < 3.0) 334 | rexml (>= 3.2.5, < 4.0) 335 | rubocop-ast (>= 1.18.0, < 2.0) 336 | ruby-progressbar (~> 1.7) 337 | unicode-display_width (>= 1.4.0, < 3.0) 338 | rubocop-ast (1.18.0) 339 | parser (>= 3.1.1.0) 340 | ruby-progressbar (1.11.0) 341 | ruby2_keywords (0.0.5) 342 | rubyzip (2.3.2) 343 | sassc (2.4.0) 344 | ffi (~> 1.9) 345 | sassc-rails (2.1.2) 346 | railties (>= 4.0.0) 347 | sassc (>= 2.0) 348 | sprockets (> 3.0) 349 | sprockets-rails 350 | tilt 351 | selenium-webdriver (4.2.1) 352 | childprocess (>= 0.5, < 5.0) 353 | rexml (~> 3.2, >= 3.2.5) 354 | rubyzip (>= 1.2.2, < 3.0) 355 | websocket (~> 1.0) 356 | sprockets (4.0.3) 357 | concurrent-ruby (~> 1.0) 358 | rack (> 1, < 3) 359 | sprockets-rails (3.4.2) 360 | actionpack (>= 5.2) 361 | activesupport (>= 5.2) 362 | sprockets (>= 3.0.0) 363 | strscan (3.0.3) 364 | thor (1.2.1) 365 | tilt (2.0.10) 366 | timeout (0.3.0) 367 | turbo-rails (1.1.1) 368 | actionpack (>= 6.0.0) 369 | activejob (>= 6.0.0) 370 | railties (>= 6.0.0) 371 | tzinfo (2.0.4) 372 | concurrent-ruby (~> 1.0) 373 | unicode-display_width (2.1.0) 374 | warden (1.2.9) 375 | rack (>= 2.0.9) 376 | web-console (4.2.0) 377 | actionview (>= 6.0.0) 378 | activemodel (>= 6.0.0) 379 | bindex (>= 0.4.0) 380 | railties (>= 6.0.0) 381 | webdrivers (5.0.0) 382 | nokogiri (~> 1.6) 383 | rubyzip (>= 1.3.0) 384 | selenium-webdriver (~> 4.0) 385 | websocket (1.2.9) 386 | websocket-driver (0.7.5) 387 | websocket-extensions (>= 0.1.0) 388 | websocket-extensions (0.1.5) 389 | xpath (3.2.0) 390 | nokogiri (~> 1.8) 391 | zeitwerk (2.5.4) 392 | 393 | PLATFORMS 394 | arm64-darwin-21 395 | 396 | DEPENDENCIES 397 | bcrypt (~> 3.1.7) 398 | bootsnap 399 | cancancan (= 3.3) 400 | capybara 401 | cssbundling-rails (~> 1.1) 402 | debug 403 | devise (= 4.8.1) 404 | doorkeeper (~> 5.5) 405 | factory_bot_rails 406 | faker! 407 | grape (~> 1.6) 408 | jbuilder 409 | jsbundling-rails (~> 1.0) 410 | pg (= 1.3.5) 411 | puma (~> 5.0) 412 | rails (~> 7.0.3) 413 | rails_admin (= 3.0) 414 | ransack (~> 3.2) 415 | redis (~> 4.0) 416 | redis-rails (~> 5.0) 417 | rspec-core! 418 | rspec-expectations! 419 | rspec-mocks! 420 | rspec-rails! 421 | rspec-support! 422 | rubocop (~> 1.30) 423 | sassc-rails 424 | selenium-webdriver 425 | sprockets-rails 426 | tzinfo-data 427 | web-console 428 | webdrivers 429 | 430 | RUBY VERSION 431 | ruby 3.0.2p107 432 | 433 | BUNDLED WITH 434 | 2.2.22 435 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Assuming you have not yet modified this file, each configuration option below 4 | # is set to its default value. Note that some are commented out while others 5 | # are not: uncommented lines are intended to protect your configuration from 6 | # breaking changes in upgrades (i.e., in the event that future versions of 7 | # Devise change the default values for those options). 8 | # 9 | # Use this hook to configure devise mailer, warden hooks and so forth. 10 | # Many of these configuration options can be set straight in your model. 11 | Devise.setup do |config| 12 | # The secret key used by Devise. Devise uses this key to generate 13 | # random tokens. Changing this key will render invalid all existing 14 | # confirmation, reset password and unlock tokens in the database. 15 | # Devise will use the `secret_key_base` as its `secret_key` 16 | # by default. You can change it below and use your own secret key. 17 | # config.secret_key = 'f053188a0728b0f69dbbc89d9dfc42c3d72df82fe0ee7e20f9da5efa5dbce615d4abf7e9733bb66d3c2d396a45a20e4776b0907542faa511c560e886dd30cd1e' 18 | 19 | # ==> Controller configuration 20 | # Configure the parent class to the devise controllers. 21 | # config.parent_controller = 'DeviseController' 22 | 23 | # ==> Mailer Configuration 24 | # Configure the e-mail address which will be shown in Devise::Mailer, 25 | # note that it will be overwritten if you use your own mailer class 26 | # with default "from" parameter. 27 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 28 | 29 | # Configure the class responsible to send e-mails. 30 | # config.mailer = 'Devise::Mailer' 31 | 32 | # Configure the parent class responsible to send e-mails. 33 | # config.parent_mailer = 'ActionMailer::Base' 34 | 35 | # ==> ORM configuration 36 | # Load and configure the ORM. Supports :active_record (default) and 37 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 38 | # available as additional gems. 39 | require 'devise/orm/active_record' 40 | 41 | # ==> Configuration for any authentication mechanism 42 | # Configure which keys are used when authenticating a user. The default is 43 | # just :email. You can configure it to use [:username, :subdomain], so for 44 | # authenticating a user, both parameters are required. Remember that those 45 | # parameters are used only when authenticating and not when retrieving from 46 | # session. If you need permissions, you should implement that in a before filter. 47 | # You can also supply a hash where the value is a boolean determining whether 48 | # or not authentication should be aborted when the value is not present. 49 | # config.authentication_keys = [:email] 50 | 51 | # Configure parameters from the request object used for authentication. Each entry 52 | # given should be a request method and it will automatically be passed to the 53 | # find_for_authentication method and considered in your model lookup. For instance, 54 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 55 | # The same considerations mentioned for authentication_keys also apply to request_keys. 56 | # config.request_keys = [] 57 | 58 | # Configure which authentication keys should be case-insensitive. 59 | # These keys will be downcased upon creating or modifying a user and when used 60 | # to authenticate or find a user. Default is :email. 61 | config.case_insensitive_keys = [:email] 62 | 63 | # Configure which authentication keys should have whitespace stripped. 64 | # These keys will have whitespace before and after removed upon creating or 65 | # modifying a user and when used to authenticate or find a user. Default is :email. 66 | config.strip_whitespace_keys = [:email] 67 | 68 | # Tell if authentication through request.params is enabled. True by default. 69 | # It can be set to an array that will enable params authentication only for the 70 | # given strategies, for example, `config.params_authenticatable = [:database]` will 71 | # enable it only for database (email + password) authentication. 72 | # config.params_authenticatable = true 73 | 74 | # Tell if authentication through HTTP Auth is enabled. False by default. 75 | # It can be set to an array that will enable http authentication only for the 76 | # given strategies, for example, `config.http_authenticatable = [:database]` will 77 | # enable it only for database authentication. 78 | # For API-only applications to support authentication "out-of-the-box", you will likely want to 79 | # enable this with :database unless you are using a custom strategy. 80 | # The supported strategies are: 81 | # :database = Support basic authentication with authentication key + password 82 | # config.http_authenticatable = false 83 | 84 | # If 401 status code should be returned for AJAX requests. True by default. 85 | # config.http_authenticatable_on_xhr = true 86 | 87 | # The realm used in Http Basic Authentication. 'Application' by default. 88 | # config.http_authentication_realm = 'Application' 89 | 90 | # It will change confirmation, password recovery and other workflows 91 | # to behave the same regardless if the e-mail provided was right or wrong. 92 | # Does not affect registerable. 93 | # config.paranoid = true 94 | 95 | # By default Devise will store the user in session. You can skip storage for 96 | # particular strategies by setting this option. 97 | # Notice that if you are skipping storage for all authentication paths, you 98 | # may want to disable generating routes to Devise's sessions controller by 99 | # passing skip: :sessions to `devise_for` in your config/routes.rb 100 | config.skip_session_storage = [:http_auth] 101 | 102 | # By default, Devise cleans up the CSRF token on authentication to 103 | # avoid CSRF token fixation attacks. This means that, when using AJAX 104 | # requests for sign in and sign up, you need to get a new CSRF token 105 | # from the server. You can disable this option at your own risk. 106 | # config.clean_up_csrf_token_on_authentication = true 107 | 108 | # When false, Devise will not attempt to reload routes on eager load. 109 | # This can reduce the time taken to boot the app but if your application 110 | # requires the Devise mappings to be loaded during boot time the application 111 | # won't boot properly. 112 | # config.reload_routes = true 113 | 114 | # ==> Configuration for :database_authenticatable 115 | # For bcrypt, this is the cost for hashing the password and defaults to 12. If 116 | # using other algorithms, it sets how many times you want the password to be hashed. 117 | # The number of stretches used for generating the hashed password are stored 118 | # with the hashed password. This allows you to change the stretches without 119 | # invalidating existing passwords. 120 | # 121 | # Limiting the stretches to just one in testing will increase the performance of 122 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 123 | # a value less than 10 in other environments. Note that, for bcrypt (the default 124 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 125 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 126 | config.stretches = Rails.env.test? ? 1 : 12 127 | 128 | # Set up a pepper to generate the hashed password. 129 | # config.pepper = '96837016e5d7336b340d9de8e316fccac39c22e5a24a5131684c4c20e6b9baecdf0c264e0db4340b24c68fa649385d0890613f8c9f46250af2d679ce95086e98' 130 | 131 | # Send a notification to the original email when the user's email is changed. 132 | # config.send_email_changed_notification = false 133 | 134 | # Send a notification email when the user's password is changed. 135 | # config.send_password_change_notification = false 136 | 137 | # ==> Configuration for :confirmable 138 | # A period that the user is allowed to access the website even without 139 | # confirming their account. For instance, if set to 2.days, the user will be 140 | # able to access the website for two days without confirming their account, 141 | # access will be blocked just in the third day. 142 | # You can also set it to nil, which will allow the user to access the website 143 | # without confirming their account. 144 | # Default is 0.days, meaning the user cannot access the website without 145 | # confirming their account. 146 | # config.allow_unconfirmed_access_for = 2.days 147 | 148 | # A period that the user is allowed to confirm their account before their 149 | # token becomes invalid. For example, if set to 3.days, the user can confirm 150 | # their account within 3 days after the mail was sent, but on the fourth day 151 | # their account can't be confirmed with the token any more. 152 | # Default is nil, meaning there is no restriction on how long a user can take 153 | # before confirming their account. 154 | # config.confirm_within = 3.days 155 | 156 | # If true, requires any email changes to be confirmed (exactly the same way as 157 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 158 | # db field (see migrations). Until confirmed, new email is stored in 159 | # unconfirmed_email column, and copied to email column on successful confirmation. 160 | config.reconfirmable = true 161 | 162 | # Defines which key will be used when confirming an account 163 | # config.confirmation_keys = [:email] 164 | 165 | # ==> Configuration for :rememberable 166 | # The time the user will be remembered without asking for credentials again. 167 | # config.remember_for = 2.weeks 168 | 169 | # Invalidates all the remember me tokens when the user signs out. 170 | config.expire_all_remember_me_on_sign_out = true 171 | 172 | # If true, extends the user's remember period when remembered via cookie. 173 | # config.extend_remember_period = false 174 | 175 | # Options to be passed to the created cookie. For instance, you can set 176 | # secure: true in order to force SSL only cookies. 177 | # config.rememberable_options = {} 178 | 179 | # ==> Configuration for :validatable 180 | # Range for password length. 181 | config.password_length = 6..128 182 | 183 | # Email regex used to validate email formats. It simply asserts that 184 | # one (and only one) @ exists in the given string. This is mainly 185 | # to give user feedback and not to assert the e-mail validity. 186 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 187 | 188 | # ==> Configuration for :timeoutable 189 | # The time you want to timeout the user session without activity. After this 190 | # time the user will be asked for credentials again. Default is 30 minutes. 191 | # config.timeout_in = 30.minutes 192 | 193 | # ==> Configuration for :lockable 194 | # Defines which strategy will be used to lock an account. 195 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 196 | # :none = No lock strategy. You should handle locking by yourself. 197 | # config.lock_strategy = :failed_attempts 198 | 199 | # Defines which key will be used when locking and unlocking an account 200 | # config.unlock_keys = [:email] 201 | 202 | # Defines which strategy will be used to unlock an account. 203 | # :email = Sends an unlock link to the user email 204 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 205 | # :both = Enables both strategies 206 | # :none = No unlock strategy. You should handle unlocking by yourself. 207 | # config.unlock_strategy = :both 208 | 209 | # Number of authentication tries before locking an account if lock_strategy 210 | # is failed attempts. 211 | # config.maximum_attempts = 20 212 | 213 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 214 | # config.unlock_in = 1.hour 215 | 216 | # Warn on the last attempt before the account is locked. 217 | # config.last_attempt_warning = true 218 | 219 | # ==> Configuration for :recoverable 220 | # 221 | # Defines which key will be used when recovering the password for an account 222 | # config.reset_password_keys = [:email] 223 | 224 | # Time interval you can reset your password with a reset password key. 225 | # Don't put a too small interval or your users won't have the time to 226 | # change their passwords. 227 | config.reset_password_within = 6.hours 228 | 229 | # When set to false, does not sign a user in automatically after their password is 230 | # reset. Defaults to true, so a user is signed in automatically after a reset. 231 | # config.sign_in_after_reset_password = true 232 | 233 | # ==> Configuration for :encryptable 234 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 235 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 236 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 237 | # for default behavior) and :restful_authentication_sha1 (then you should set 238 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 239 | # 240 | # Require the `devise-encryptable` gem when using anything other than bcrypt 241 | # config.encryptor = :sha512 242 | 243 | # ==> Scopes configuration 244 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 245 | # "users/sessions/new". It's turned off by default because it's slower if you 246 | # are using only default views. 247 | # config.scoped_views = false 248 | 249 | # Configure the default scope given to Warden. By default it's the first 250 | # devise role declared in your routes (usually :user). 251 | # config.default_scope = :user 252 | 253 | # Set this configuration to false if you want /users/sign_out to sign out 254 | # only the current scope. By default, Devise signs out all scopes. 255 | # config.sign_out_all_scopes = true 256 | 257 | # ==> Navigation configuration 258 | # Lists the formats that should be treated as navigational. Formats like 259 | # :html, should redirect to the sign in page when the user does not have 260 | # access, but formats like :xml or :json, should return 401. 261 | # 262 | # If you have any extra navigational formats, like :iphone or :mobile, you 263 | # should add them to the navigational formats lists. 264 | # 265 | # The "*/*" below is required to match Internet Explorer requests. 266 | # config.navigational_formats = ['*/*', :html] 267 | 268 | # The default HTTP method used to sign out a resource. Default is :delete. 269 | config.sign_out_via = :get 270 | 271 | # ==> OmniAuth 272 | # Add a new OmniAuth provider. Check the wiki for more information on setting 273 | # up on your models and hooks. 274 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 275 | 276 | # ==> Warden configuration 277 | # If you want to use other strategies, that are not supported by Devise, or 278 | # change the failure app, you can configure them inside the config.warden block. 279 | # 280 | # config.warden do |manager| 281 | # manager.intercept_401 = false 282 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 283 | # end 284 | 285 | # ==> Mountable engine configurations 286 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 287 | # is mountable, there are some extra configurations to be taken into account. 288 | # The following options are available, assuming the engine is mounted as: 289 | # 290 | # mount MyEngine, at: '/my_engine' 291 | # 292 | # The router that invoked `devise_for`, in the example above, would be: 293 | # config.router_name = :my_engine 294 | # 295 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 296 | # so you need to do it manually. For the users scope, it would be: 297 | # config.omniauth_path_prefix = '/my_engine/users/auth' 298 | 299 | # ==> Turbolinks configuration 300 | # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: 301 | # 302 | # ActiveSupport.on_load(:devise_failure_app) do 303 | # include Turbolinks::Controller 304 | # end 305 | 306 | # ==> Configuration for :registerable 307 | 308 | # When set to false, does not sign a user in automatically after their password is 309 | # changed. Defaults to true, so a user is signed in automatically after changing a password. 310 | # config.sign_in_after_change_password = true 311 | end 312 | -------------------------------------------------------------------------------- /config/initializers/doorkeeper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Doorkeeper.configure do 4 | # Change the ORM that doorkeeper will use (requires ORM extensions installed). 5 | # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms 6 | orm :active_record 7 | 8 | resource_owner_from_credentials do |_routes| 9 | User.authenticate(params[:email], params[:password]) 10 | end 11 | 12 | skip_client_authentication_for_password_grant true 13 | 14 | # This block will be called to check whether the resource owner is authenticated or not. 15 | # resource_owner_authenticator do 16 | # raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" 17 | # # Put your resource owner authentication logic here. 18 | # # Example implementation: 19 | # # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) 20 | # end 21 | 22 | # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb 23 | # file then you need to declare this block in order to restrict access to the web interface for 24 | # adding oauth authorized applications. In other case it will return 403 Forbidden response 25 | # every time somebody will try to access the admin web interface. 26 | # 27 | # admin_authenticator do 28 | # # Put your admin authentication logic here. 29 | # # Example implementation: 30 | # 31 | # if current_user 32 | # head :forbidden unless current_user.admin? 33 | # else 34 | # redirect_to sign_in_url 35 | # end 36 | # end 37 | 38 | # You can use your own model classes if you need to extend (or even override) default 39 | # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. 40 | # 41 | # Be default Doorkeeper ActiveRecord ORM uses it's own classes: 42 | # 43 | # access_token_class "Doorkeeper::AccessToken" 44 | # access_grant_class "Doorkeeper::AccessGrant" 45 | # application_class "Doorkeeper::Application" 46 | # 47 | # Don't forget to include Doorkeeper ORM mixins into your custom models: 48 | # 49 | # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token 50 | # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant 51 | # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) 52 | # 53 | # For example: 54 | # 55 | # access_token_class "MyAccessToken" 56 | # 57 | # class MyAccessToken < ApplicationRecord 58 | # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken 59 | # 60 | # self.table_name = "hey_i_wanna_my_name" 61 | # 62 | # def destroy_me! 63 | # destroy 64 | # end 65 | # end 66 | 67 | # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. 68 | # By default this option is disabled. 69 | # 70 | # Make sure you properly setup you database and have all the required columns (run 71 | # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails 72 | # migrations). 73 | # 74 | # If this option enabled, Doorkeeper will store not only Resource Owner primary key 75 | # value, but also it's type (class name). See "Polymorphic Associations" section of 76 | # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations 77 | # 78 | # [NOTE] If you apply this option on already existing project don't forget to manually 79 | # update `resource_owner_type` column in the database and fix migration template as it will 80 | # set NOT NULL constraint for Access Grants table. 81 | # 82 | # use_polymorphic_resource_owner 83 | 84 | # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might 85 | # want to use API mode that will skip all the views management and change the way how 86 | # Doorkeeper responds to a requests. 87 | # 88 | # api_only 89 | 90 | # Enforce token request content type to application/x-www-form-urlencoded. 91 | # It is not enabled by default to not break prior versions of the gem. 92 | # 93 | # enforce_content_type 94 | 95 | # Authorization Code expiration time (default: 10 minutes). 96 | # 97 | # authorization_code_expires_in 10.minutes 98 | 99 | # Access token expiration time (default: 2 hours). 100 | # If you want to disable expiration, set this to `nil`. 101 | # 102 | # access_token_expires_in 2.hours 103 | 104 | # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in 105 | # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to 106 | # +access_token_expires_in+ configuration option value. If you really need to issue a 107 | # non-expiring access token (which is not recommended) then you need to return 108 | # Float::INFINITY from this block. 109 | # 110 | # `context` has the following properties available: 111 | # 112 | # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) 113 | # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) 114 | # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) 115 | # * `resource_owner` - authorized resource owner instance (if present) 116 | # 117 | # custom_access_token_expires_in do |context| 118 | # context.client.additional_settings.implicit_oauth_expiration 119 | # end 120 | 121 | # Use a custom class for generating the access token. 122 | # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator 123 | # 124 | # access_token_generator '::Doorkeeper::JWT' 125 | 126 | # The controller +Doorkeeper::ApplicationController+ inherits from. 127 | # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to 128 | # +ActionController::API+. The return value of this option must be a stringified class name. 129 | # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers 130 | # 131 | # base_controller 'ApplicationController' 132 | 133 | # Reuse access token for the same resource owner within an application (disabled by default). 134 | # 135 | # This option protects your application from creating new tokens before old valid one becomes 136 | # expired so your database doesn't bloat. Keep in mind that when this option is `on` Doorkeeper 137 | # doesn't updates existing token expiration time, it will create a new token instead. 138 | # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 139 | # 140 | # You can not enable this option together with +hash_token_secrets+. 141 | # 142 | # reuse_access_token 143 | 144 | # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching 145 | # token using `matching_token_for` Access Token API that searches for valid records 146 | # in batches in order not to pollute the memory with all the database records. By default 147 | # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value 148 | # depending on your needs and server capabilities. 149 | # 150 | # token_lookup_batch_size 10_000 151 | 152 | # Set a limit for token_reuse if using reuse_access_token option 153 | # 154 | # This option limits token_reusability to some extent. 155 | # If not set then access_token will be reused unless it expires. 156 | # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 157 | # 158 | # This option should be a percentage(i.e. (0,100]) 159 | # 160 | # token_reuse_limit 100 161 | 162 | # Only allow one valid access token obtained via client credentials 163 | # per client. If a new access token is obtained before the old one 164 | # expired, the old one gets revoked (disabled by default) 165 | # 166 | # When enabling this option, make sure that you do not expect multiple processes 167 | # using the same credentials at the same time (e.g. web servers spanning 168 | # multiple machines and/or processes). 169 | # 170 | # revoke_previous_client_credentials_token 171 | 172 | # Hash access and refresh tokens before persisting them. 173 | # This will disable the possibility to use +reuse_access_token+ 174 | # since plain values can no longer be retrieved. 175 | # 176 | # Note: If you are already a user of doorkeeper and have existing tokens 177 | # in your installation, they will be invalid without adding 'fallback: :plain'. 178 | # 179 | # hash_token_secrets 180 | # By default, token secrets will be hashed using the 181 | # +Doorkeeper::Hashing::SHA256+ strategy. 182 | # 183 | # If you wish to use another hashing implementation, you can override 184 | # this strategy as follows: 185 | # 186 | # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' 187 | # 188 | # Keep in mind that changing the hashing function will invalidate all existing 189 | # secrets, if there are any. 190 | 191 | # Hash application secrets before persisting them. 192 | # 193 | # hash_application_secrets 194 | # 195 | # By default, applications will be hashed 196 | # with the +Doorkeeper::SecretStoring::SHA256+ strategy. 197 | # 198 | # If you wish to use bcrypt for application secret hashing, uncomment 199 | # this line instead: 200 | # 201 | # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' 202 | 203 | # When the above option is enabled, and a hashed token or secret is not found, 204 | # you can allow to fall back to another strategy. For users upgrading 205 | # doorkeeper and wishing to enable hashing, you will probably want to enable 206 | # the fallback to plain tokens. 207 | # 208 | # This will ensure that old access tokens and secrets 209 | # will remain valid even if the hashing above is enabled. 210 | # 211 | # This can be done by adding 'fallback: plain', e.g. : 212 | # 213 | # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain 214 | 215 | # Issue access tokens with refresh token (disabled by default), you may also 216 | # pass a block which accepts `context` to customize when to give a refresh 217 | # token or not. Similar to +custom_access_token_expires_in+, `context` has 218 | # the following properties: 219 | # 220 | # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) 221 | # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) 222 | # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) 223 | # 224 | use_refresh_token 225 | 226 | # Provide support for an owner to be assigned to each registered application (disabled by default) 227 | # Optional parameter confirmation: true (default: false) if you want to enforce ownership of 228 | # a registered application 229 | # NOTE: you must also run the rails g doorkeeper:application_owner generator 230 | # to provide the necessary support 231 | # 232 | # enable_application_owner confirmation: false 233 | 234 | # Define access token scopes for your provider 235 | # For more information go to 236 | # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes 237 | # 238 | # default_scopes :public 239 | # optional_scopes :write, :update 240 | 241 | # Allows to restrict only certain scopes for grant_type. 242 | # By default, all the scopes will be available for all the grant types. 243 | # 244 | # Keys to this hash should be the name of grant_type and 245 | # values should be the array of scopes for that grant type. 246 | # Note: scopes should be from configured_scopes (i.e. default or optional) 247 | # 248 | # scopes_by_grant_type password: [:write], client_credentials: [:update] 249 | 250 | # Forbids creating/updating applications with arbitrary scopes that are 251 | # not in configuration, i.e. +default_scopes+ or +optional_scopes+. 252 | # (disabled by default) 253 | # 254 | # enforce_configured_scopes 255 | 256 | # Change the way client credentials are retrieved from the request object. 257 | # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then 258 | # falls back to the `:client_id` and `:client_secret` params from the `params` object. 259 | # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated 260 | # for more information on customization 261 | # 262 | # client_credentials :from_basic, :from_params 263 | 264 | # Change the way access token is authenticated from the request object. 265 | # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then 266 | # falls back to the `:access_token` or `:bearer_token` params from the `params` object. 267 | # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated 268 | # for more information on customization 269 | # 270 | # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param 271 | 272 | # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled 273 | # by default in non-development environments). OAuth2 delegates security in 274 | # communication to the HTTPS protocol so it is wise to keep this enabled. 275 | # 276 | # Callable objects such as proc, lambda, block or any object that responds to 277 | # #call can be used in order to allow conditional checks (to allow non-SSL 278 | # redirects to localhost for example). 279 | # 280 | # force_ssl_in_redirect_uri !Rails.env.development? 281 | # 282 | # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } 283 | 284 | # Specify what redirect URI's you want to block during Application creation. 285 | # Any redirect URI is allowed by default. 286 | # 287 | # You can use this option in order to forbid URI's with 'javascript' scheme 288 | # for example. 289 | # 290 | # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } 291 | 292 | # Allows to set blank redirect URIs for Applications in case Doorkeeper configured 293 | # to use URI-less OAuth grant flows like Client Credentials or Resource Owner 294 | # Password Credentials. The option is on by default and checks configured grant 295 | # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` 296 | # column for `oauth_applications` database table. 297 | # 298 | # You can completely disable this feature with: 299 | # 300 | allow_blank_redirect_uri true 301 | # 302 | # Or you can define your custom check: 303 | # 304 | # allow_blank_redirect_uri do |grant_flows, client| 305 | # client.superapp? 306 | # end 307 | 308 | # Specify how authorization errors should be handled. 309 | # By default, doorkeeper renders json errors when access token 310 | # is invalid, expired, revoked or has invalid scopes. 311 | # 312 | # If you want to render error response yourself (i.e. rescue exceptions), 313 | # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken 314 | # or following specific errors: 315 | # 316 | # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, 317 | # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown 318 | # 319 | # handle_auth_errors :raise 320 | 321 | # Customize token introspection response. 322 | # Allows to add your own fields to default one that are required by the OAuth spec 323 | # for the introspection response. It could be `sub`, `aud` and so on. 324 | # This configuration option can be a proc, lambda or any Ruby object responds 325 | # to `.call` method and result of it's invocation must be a Hash. 326 | # 327 | # custom_introspection_response do |token, context| 328 | # { 329 | # "sub": "Z5O3upPC88QrAjx00dis", 330 | # "aud": "https://protected.example.net/resource", 331 | # "username": User.find(token.resource_owner_id).username 332 | # } 333 | # end 334 | # 335 | # or 336 | # 337 | # custom_introspection_response CustomIntrospectionResponder 338 | 339 | # Specify what grant flows are enabled in array of Strings. The valid 340 | # strings and the flows they enable are: 341 | # 342 | # "authorization_code" => Authorization Code Grant Flow 343 | # "implicit" => Implicit Grant Flow 344 | # "password" => Resource Owner Password Credentials Grant Flow 345 | # "client_credentials" => Client Credentials Grant Flow 346 | # 347 | # If not specified, Doorkeeper enables authorization_code and 348 | # client_credentials. 349 | # 350 | # implicit and password grant flows have risks that you should understand 351 | # before enabling: 352 | # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2 353 | # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.3 354 | # 355 | grant_flows %w[password] 356 | 357 | # Allows to customize OAuth grant flows that +each+ application support. 358 | # You can configure a custom block (or use a class respond to `#call`) that must 359 | # return `true` in case Application instance supports requested OAuth grant flow 360 | # during the authorization request to the server. This configuration +doesn't+ 361 | # set flows per application, it only allows to check if application supports 362 | # specific grant flow. 363 | # 364 | # For example you can add an additional database column to `oauth_applications` table, 365 | # say `t.array :grant_flows, default: []`, and store allowed grant flows that can 366 | # be used with this application there. Then when authorization requested Doorkeeper 367 | # will call this block to check if specific Application (passed with client_id and/or 368 | # client_secret) is allowed to perform the request for the specific grant type 369 | # (authorization, password, client_credentials, etc). 370 | # 371 | # Example of the block: 372 | # 373 | # ->(flow, client) { client.grant_flows.include?(flow) } 374 | # 375 | # In case this option invocation result is `false`, Doorkeeper server returns 376 | # :unauthorized_client error and stops the request. 377 | # 378 | # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call 379 | # @return [Boolean] `true` if allow or `false` if forbid the request 380 | # 381 | # allow_grant_flow_for_client do |grant_flow, client| 382 | # # `grant_flows` is an Array column with grant 383 | # # flows that application supports 384 | # 385 | # client.grant_flows.include?(grant_flow) 386 | # end 387 | 388 | # If you need arbitrary Resource Owner-Client authorization you can enable this option 389 | # and implement the check your need. Config option must respond to #call and return 390 | # true in case resource owner authorized for the specific application or false in other 391 | # cases. 392 | # 393 | # Be default all Resource Owners are authorized to any Client (application). 394 | # 395 | # authorize_resource_owner_for_client do |client, resource_owner| 396 | # resource_owner.admin? || client.owners_allowlist.include?(resource_owner) 397 | # end 398 | 399 | # Hook into the strategies' request & response life-cycle in case your 400 | # application needs advanced customization or logging: 401 | # 402 | # before_successful_strategy_response do |request| 403 | # puts "BEFORE HOOK FIRED! #{request}" 404 | # end 405 | # 406 | # after_successful_strategy_response do |request, response| 407 | # puts "AFTER HOOK FIRED! #{request}, #{response}" 408 | # end 409 | 410 | # Hook into Authorization flow in order to implement Single Sign Out 411 | # or add any other functionality. Inside the block you have an access 412 | # to `controller` (authorizations controller instance) and `context` 413 | # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth 414 | # or auth objects with issued token based on hook type (before or after). 415 | # 416 | # before_successful_authorization do |controller, context| 417 | # Rails.logger.info(controller.request.params.inspect) 418 | # 419 | # Rails.logger.info(context.pre_auth.inspect) 420 | # end 421 | # 422 | # after_successful_authorization do |controller, context| 423 | # controller.session[:logout_urls] << 424 | # Doorkeeper::Application 425 | # .find_by(controller.request.params.slice(:redirect_uri)) 426 | # .logout_uri 427 | # 428 | # Rails.logger.info(context.auth.inspect) 429 | # Rails.logger.info(context.issued_token) 430 | # end 431 | 432 | # Under some circumstances you might want to have applications auto-approved, 433 | # so that the user skips the authorization step. 434 | # For example if dealing with a trusted application. 435 | # 436 | skip_authorization do 437 | true 438 | end 439 | 440 | # Configure custom constraints for the Token Introspection request. 441 | # By default this configuration option allows to introspect a token by another 442 | # token of the same application, OR to introspect the token that belongs to 443 | # authorized client (from authenticated client) OR when token doesn't 444 | # belong to any client (public token). Otherwise requester has no access to the 445 | # introspection and it will return response as stated in the RFC. 446 | # 447 | # Block arguments: 448 | # 449 | # @param token [Doorkeeper::AccessToken] 450 | # token to be introspected 451 | # 452 | # @param authorized_client [Doorkeeper::Application] 453 | # authorized client (if request is authorized using Basic auth with 454 | # Client Credentials for example) 455 | # 456 | # @param authorized_token [Doorkeeper::AccessToken] 457 | # Bearer token used to authorize the request 458 | # 459 | # In case the block returns `nil` or `false` introspection responses with 401 status code 460 | # when using authorized token to introspect, or you'll get 200 with { "active": false } body 461 | # when using authorized client to introspect as stated in the 462 | # RFC 7662 section 2.2. Introspection Response. 463 | # 464 | # Using with caution: 465 | # Keep in mind that these three parameters pass to block can be nil as following case: 466 | # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. 467 | # `token` will be nil if and only if `authorized_token` is present. 468 | # So remember to use `&` or check if it is present before calling method on 469 | # them to make sure you doesn't get NoMethodError exception. 470 | # 471 | # You can define your custom check: 472 | # 473 | # allow_token_introspection do |token, authorized_client, authorized_token| 474 | # if authorized_token 475 | # # customize: require `introspection` scope 476 | # authorized_token.application == token&.application || 477 | # authorized_token.scopes.include?("introspection") 478 | # elsif token.application 479 | # # `protected_resource` is a new database boolean column, for example 480 | # authorized_client == token.application || authorized_client.protected_resource? 481 | # else 482 | # # public token (when token.application is nil, token doesn't belong to any application) 483 | # true 484 | # end 485 | # end 486 | # 487 | # Or you can completely disable any token introspection: 488 | # 489 | # allow_token_introspection false 490 | # 491 | # If you need to block the request at all, then configure your routes.rb or web-server 492 | # like nginx to forbid the request. 493 | 494 | # WWW-Authenticate Realm (default: "Doorkeeper"). 495 | # 496 | # realm "Doorkeeper" 497 | end 498 | --------------------------------------------------------------------------------