├── .github └── workflows │ └── rubyonrails.yml ├── README.md ├── backend ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── api │ │ │ └── v1 │ │ │ │ ├── android │ │ │ │ └── books_controller.rb │ │ │ │ ├── books_controller.rb │ │ │ │ ├── users │ │ │ │ └── registrations_controller.rb │ │ │ │ └── users_controller.rb │ │ ├── api_controller.rb │ │ ├── apidocs_controller.rb │ │ ├── application_controller.rb │ │ ├── books_controller.rb │ │ ├── concerns │ │ │ ├── .keep │ │ │ └── doorkeeper_registerable.rb │ │ ├── doorkeeper │ │ │ ├── application_metal_controller.rb │ │ │ └── tokens_controller.rb │ │ ├── pages_controller.rb │ │ └── swagger │ │ │ ├── controllers │ │ │ └── users │ │ │ │ ├── registrations_controller.rb │ │ │ │ └── tokens_controller.rb │ │ │ ├── inputs │ │ │ └── users │ │ │ │ ├── registration_input.rb │ │ │ │ └── token_inputs.rb │ │ │ └── responses │ │ │ ├── error_response.rb │ │ │ └── users │ │ │ ├── registration_responses.rb │ │ │ └── token_responses.rb │ ├── helpers │ │ ├── application_helper.rb │ │ ├── books_helper.rb │ │ └── pages_helper.rb │ ├── javascript │ │ ├── application.js │ │ └── controllers │ │ │ ├── application.js │ │ │ ├── hello_controller.js │ │ │ └── index.js │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ ├── book.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── user.rb │ └── views │ │ ├── books │ │ ├── _book.html.erb │ │ ├── _book.json.jbuilder │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── index.json.jbuilder │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ └── show.json.jbuilder │ │ ├── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ ├── mailer.text.erb │ │ └── swagger.html.erb │ │ └── pages │ │ └── home.html.erb ├── bin │ ├── bundle │ ├── importmap │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── credentials.yml.enc │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── importmap.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── content_security_policy.rb │ │ ├── cors.rb │ │ ├── devise.rb │ │ ├── doorkeeper.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── permissions_policy.rb │ │ ├── rswag-ui.rb │ │ └── rswag_api.rb │ ├── locales │ │ ├── devise.en.yml │ │ ├── doorkeeper.en.yml │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── routes │ │ └── api.rb │ └── storage.yml ├── db │ ├── migrate │ │ ├── 20220404025649_create_books.rb │ │ ├── 20220404025721_devise_create_users.rb │ │ ├── 20220404025750_add_role_to_user.rb │ │ └── 20220404030809_create_doorkeeper_tables.rb │ ├── schema.rb │ ├── seeds.rb │ └── seeds │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb ├── lib │ ├── assets │ │ └── .keep │ └── tasks │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── robots.txt ├── scripts │ ├── rubocop.sh │ └── swagger.sh ├── spec │ ├── integration │ │ └── books_spec.rb │ ├── rails_helper.rb │ ├── spec_helper.rb │ ├── support │ │ └── factory_bot.rb │ └── swagger_helper.rb ├── storage │ └── .keep ├── swagger │ └── v1 │ │ └── swagger.yaml ├── test │ ├── application_system_test_case.rb │ ├── channels │ │ └── application_cable │ │ │ └── connection_test.rb │ ├── controllers │ │ ├── .keep │ │ ├── api │ │ │ └── v1 │ │ │ │ └── books_controller_test.rb │ │ ├── books_controller_test.rb │ │ └── pages_controller_test.rb │ ├── factories │ │ ├── books_factory.rb │ │ ├── doorkeeper │ │ │ ├── doorkeeper_access_tokens_factory.rb │ │ │ └── doorkeeper_applications_factory.rb │ │ └── users_factory.rb │ ├── fixtures │ │ ├── books.yml │ │ ├── files │ │ │ └── .keep │ │ └── users.yml │ ├── helpers │ │ ├── .keep │ │ ├── api_helpers.rb │ │ └── doorkeeper_params.rb │ ├── integration │ │ └── .keep │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── book_test.rb │ │ └── user_test.rb │ ├── requests │ │ ├── registrations_controller_test.rb │ │ └── token_controller_test.rb │ ├── system │ │ ├── .keep │ │ └── books_test.rb │ └── test_helper.rb ├── tmp │ ├── .keep │ ├── pids │ │ └── .keep │ └── storage │ │ └── .keep └── vendor │ ├── .keep │ └── javascript │ └── .keep └── frontend ├── .eslintrc.js ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── app │ ├── api │ │ ├── axios.ts │ │ └── sessionAPI.ts │ ├── features │ │ ├── appbar │ │ │ └── AppBar.tsx │ │ ├── counter │ │ │ ├── Counter.module.css │ │ │ ├── Counter.tsx │ │ │ ├── counterAPI.ts │ │ │ ├── counterSlice.spec.ts │ │ │ └── counterSlice.ts │ │ ├── dashboard │ │ │ └── Dashboard.tsx │ │ ├── routes │ │ │ ├── PrivateRoute.tsx │ │ │ └── PublicOnlyRoute.tsx │ │ └── sessions │ │ │ ├── Login.tsx │ │ │ ├── Logout.tsx │ │ │ ├── PersistLogin.tsx │ │ │ ├── Signup.tsx │ │ │ ├── UpdateProfile.tsx │ │ │ └── sessionSlice.tsx │ ├── hooks.ts │ └── store.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.ts └── setupTests.ts └── tsconfig.json /.github/workflows/rubyonrails.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are 2 | # provided by a third-party and are governed by separate terms of service, 3 | # privacy policy, and support documentation. 4 | # 5 | # This workflow will install a prebuilt Ruby version, install dependencies, and 6 | # run tests and linters. 7 | name: "Ruby on Rails CI" 8 | on: 9 | push: 10 | branches: 11 | - "**" 12 | pull_request: 13 | branches: 14 | - "**" 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | working-directory: backend 21 | services: 22 | postgres: 23 | image: postgres:11-alpine 24 | ports: 25 | - "5432:5432" 26 | env: 27 | POSTGRES_DB: rails_test 28 | POSTGRES_USER: rails 29 | POSTGRES_PASSWORD: password 30 | env: 31 | RAILS_ENV: test 32 | DATABASE_URL: "postgres://rails:password@localhost:5432/rails_test" 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v2 36 | # Add or replace dependency steps here 37 | - name: Install Ruby and gems 38 | uses: ruby/setup-ruby@v1 39 | with: 40 | ruby-version: '3.1.0' 41 | bundler-cache: true 42 | - name: Bundle install 43 | run: bundle install 44 | # Add or replace database setup steps here 45 | - name: Set up database schema 46 | run: bin/rails db:schema:load 47 | # Seed the db 48 | - name: Seed database 49 | run: bin/rails db:seed environment=test 50 | # Add or replace test runners here 51 | - name: Run specs 52 | run: bin/rake 53 | - name: Run Rails tests 54 | run: bundle exec rails test 55 | 56 | lint: 57 | runs-on: ubuntu-latest 58 | defaults: 59 | run: 60 | working-directory: backend 61 | steps: 62 | - name: Checkout code 63 | uses: actions/checkout@v2 64 | - name: Install Ruby and gems 65 | uses: ruby/setup-ruby@v1 66 | with: 67 | ruby-version: '3.1.0' 68 | bundler-cache: true 69 | - name: Bundle install 70 | run: bundle install 71 | # Add or replace any other lints here 72 | - name: Security audit dependencies 73 | run: bundle exec bundle audit --update 74 | - name: Security audit application code 75 | run: bundle exec brakeman -q -w2 76 | - name: Lint Ruby files 77 | run: bundle exec rubocop --parallel 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doorkeeper Starter Template 2 | Repository for a doorkeeper backend. To be used as a template for projects that need a Ruby on Rails API backend 3 | 4 | ## Technologies Used 5 | - Backend 6 | - Ruby On Rails 7 | - Devise 8 | - Doorkeeper -------------------------------------------------------------------------------- /backend/.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/pids/* 22 | !/tmp/pids/ 23 | !/tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | /tmp/storage/* 29 | !/tmp/storage/ 30 | !/tmp/storage/.keep 31 | 32 | /public/assets 33 | 34 | # Ignore master key for decrypting credentials and more. 35 | /config/master.key 36 | -------------------------------------------------------------------------------- /backend/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /backend/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | -------------------------------------------------------------------------------- /backend/.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2022-04-08 20:42:23 UTC using RuboCop version 1.27.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: AllowComments. 11 | Lint/EmptyFile: 12 | Exclude: 13 | - 'db/seeds/production.rb' 14 | 15 | # Offense count: 1 16 | # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. 17 | Metrics/AbcSize: 18 | Max: 23 19 | 20 | # Offense count: 2 21 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 22 | # IgnoredMethods: refine 23 | Metrics/BlockLength: 24 | Max: 58 25 | 26 | # Offense count: 1 27 | # Configuration parameters: IgnoredMethods. 28 | Metrics/CyclomaticComplexity: 29 | Max: 9 30 | 31 | # Offense count: 9 32 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 33 | Metrics/MethodLength: 34 | Max: 28 35 | 36 | # Offense count: 1 37 | # Configuration parameters: IgnoredMethods. 38 | Metrics/PerceivedComplexity: 39 | Max: 9 40 | 41 | # Offense count: 1 42 | # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. 43 | # CheckDefinitionPathHierarchyRoots: lib, spec, test, src 44 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 45 | Naming/FileName: 46 | Exclude: 47 | - 'config/initializers/rswag-ui.rb' 48 | 49 | # Offense count: 1 50 | # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. 51 | # NamePrefix: is_, has_, have_ 52 | # ForbiddenPrefixes: is_, has_, have_ 53 | # AllowedMethods: is_a? 54 | # MethodDefinitionMacros: define_method, define_singleton_method 55 | Naming/PredicateName: 56 | Exclude: 57 | - 'spec/**/*' 58 | - 'app/helpers/application_helper.rb' 59 | 60 | # Offense count: 16 61 | # Configuration parameters: AllowedConstants. 62 | Style/Documentation: 63 | Enabled: false 64 | 65 | # Offense count: 3 66 | # This cop supports safe auto-correction (--auto-correct). 67 | Style/IfUnlessModifier: 68 | Exclude: 69 | - 'bin/bundle' 70 | - 'db/seeds/development.rb' 71 | - 'db/seeds/test.rb' 72 | 73 | # Offense count: 3 74 | # This cop supports safe auto-correction (--auto-correct). 75 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 76 | # URISchemes: http, https 77 | Layout/LineLength: 78 | Max: 198 79 | -------------------------------------------------------------------------------- /backend/.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.0.3 2 | -------------------------------------------------------------------------------- /backend/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.1.0' 7 | 8 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 9 | gem 'rails', '~> 7.0.2', '>= 7.0.2.3' 10 | 11 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 12 | gem 'sprockets-rails' 13 | 14 | # Use pg as the database for Active Record 15 | gem 'pg', '~> 1.1' 16 | 17 | # Use the Puma web server [https://github.com/puma/puma] 18 | gem 'puma', '~> 5.0' 19 | 20 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 21 | gem 'importmap-rails' 22 | 23 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 24 | gem 'turbo-rails' 25 | 26 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 27 | gem 'stimulus-rails' 28 | 29 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 30 | gem 'jbuilder' 31 | 32 | # Use Redis adapter to run Action Cable in production 33 | gem 'redis', '~> 4.0' 34 | 35 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 36 | # gem "kredis" 37 | 38 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 39 | # gem "bcrypt", "~> 3.1.7" 40 | 41 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 42 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 43 | 44 | # Reduces boot times through caching; required in config/boot.rb 45 | gem 'bootsnap', require: false 46 | 47 | # Use Sass to process CSS 48 | # gem "sassc-rails" 49 | 50 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 51 | # gem "image_processing", "~> 1.2" 52 | 53 | group :development, :test do 54 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 55 | gem 'debug', platforms: %i[mri mingw x64_mingw] 56 | # A library for generating fake data such as names, addresses, and phone numbers [https://github.com/faker-ruby/faker] 57 | gem 'faker', git: 'https://github.com/faker-ruby/faker.git', branch: 'master' 58 | # Required for RSpec specs 59 | gem 'rspec-rails' 60 | # Security tools 61 | gem 'brakeman' 62 | gem 'bundler-audit' 63 | gem 'ruby_audit' 64 | # Linting 65 | gem 'rubocop' 66 | gem 'rubocop-rails' 67 | gem 'rubocop-rspec' 68 | end 69 | 70 | group :development do 71 | # Use console on exceptions pages [https://github.com/rails/web-console] 72 | gem 'web-console' 73 | 74 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 75 | # gem "rack-mini-profiler" 76 | 77 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 78 | # gem "spring" 79 | end 80 | 81 | group :test do 82 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 83 | gem 'capybara' 84 | gem 'selenium-webdriver' 85 | gem 'webdrivers' 86 | # A library for setting up Ruby objects as test data [https://github.com/thoughtbot/factory_bot_rails] 87 | gem 'factory_bot_rails' 88 | end 89 | 90 | gem 'devise', '~> 4.8' 91 | 92 | gem 'doorkeeper', '~> 5.5' 93 | 94 | gem 'rack-cors', '~> 1.1' 95 | 96 | gem 'rswag', '~> 2.5' 97 | 98 | gem 'swagger-blocks', '~> 3.0' 99 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | # Tutorial 4 | Tutorial available [here](https://youtu.be/Kwm4Edvlqhw) 5 | 6 | # LICENSE 7 | 8 | ``` 9 | MIT License 10 | 11 | Copyright (c) 2022 Nejdet Kadir Bektaş 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | ``` 31 | # doorkeeper_template 32 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../../javascript .js 4 | //= link_tree ../../../vendor/javascript .js 5 | -------------------------------------------------------------------------------- /backend/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/app/assets/images/.keep -------------------------------------------------------------------------------- /backend/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /backend/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /backend/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /backend/app/controllers/api/v1/android/books_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Android 6 | class BooksController < ApiController 7 | include ApplicationHelper 8 | before_action :set_book, only: %i[show] 9 | before_action :is_admin? 10 | 11 | # GET /books or /books.json 12 | def index 13 | @books = Book.all 14 | render json: @books 15 | end 16 | 17 | # GET /books/1 or /books/1.json 18 | def show 19 | render json: @book 20 | end 21 | 22 | private 23 | 24 | # Use callbacks to share common setup or constraints between actions. 25 | def set_book 26 | @book = Book.find(params[:id]) 27 | end 28 | 29 | # Only allow a list of trusted parameters through. 30 | def book_params 31 | params.require(:book) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /backend/app/controllers/api/v1/books_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class BooksController < ApiController 6 | before_action :set_book, only: %i[show edit update destroy] 7 | 8 | # GET /books or /books.json 9 | def index 10 | @books = Book.all 11 | render json: @books 12 | end 13 | 14 | # GET /books/1 or /books/1.json 15 | def show 16 | render json: @book 17 | end 18 | 19 | # GET /books/new 20 | def new 21 | render json: @book = Book.new 22 | end 23 | 24 | # GET /books/1/edit 25 | def edit 26 | render json: @book 27 | end 28 | 29 | # POST /books or /books.json 30 | def create 31 | @book = Book.new(book_params) 32 | 33 | respond_to do |format| 34 | if @book.save 35 | format.html { redirect_to api_v1_book_url(@book), notice: 'Book was successfully created.' } 36 | format.json { render :show, status: :created, location: @book } 37 | else 38 | format.html { render :new, status: :unprocessable_entity } 39 | format.json { render json: @book.errors, status: :unprocessable_entity } 40 | end 41 | end 42 | end 43 | 44 | # PATCH/PUT /books/1 or /books/1.json 45 | def update 46 | respond_to do |format| 47 | if @book.update(book_params) 48 | format.html { redirect_to api_v1_book_url(@book), notice: 'Book was successfully updated.' } 49 | format.json { render :show, status: :ok, location: @book } 50 | else 51 | format.html { render :edit, status: :unprocessable_entity } 52 | format.json { render json: @book.errors, status: :unprocessable_entity } 53 | end 54 | end 55 | end 56 | 57 | # DELETE /books/1 or /books/1.json 58 | def destroy 59 | @book.destroy 60 | 61 | respond_to do |format| 62 | format.html { redirect_to api_v1_books_url, notice: 'Book was successfully destroyed.' } 63 | format.json { head :no_content } 64 | end 65 | end 66 | 67 | private 68 | 69 | # Use callbacks to share common setup or constraints between actions. 70 | def set_book 71 | @book = Book.find_by_id(params[:id]) 72 | render json: { error: 'Book not found' }, status: :not_found if @book.nil? 73 | end 74 | 75 | # Only allow a list of trusted parameters through. 76 | def book_params 77 | params.require(:book).permit(:title, :body) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /backend/app/controllers/api/v1/users/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Users 6 | class RegistrationsController < ApiController 7 | skip_before_action :doorkeeper_authorize!, only: %i[create] 8 | 9 | include DoorkeeperRegisterable 10 | 11 | def create 12 | client_app = Doorkeeper::Application.find_by(uid: params[:client_id]) 13 | unless client_app 14 | return render json: { error: 'Client Not Found. Check Provided Client Id.' }, 15 | status: :unauthorized 16 | end 17 | 18 | allowed_params = user_params.except(:client_id) 19 | user = User.new(allowed_params) 20 | 21 | if user.save 22 | render json: render_user(user, client_app), status: :ok 23 | else 24 | 25 | render json: { errors: user.errors.full_messages }, status: :unprocessable_entity 26 | end 27 | end 28 | 29 | def update_profile 30 | user = current_user 31 | 32 | client_app = Doorkeeper::Application.find_by(uid: params[:client_id]) 33 | unless client_app 34 | return render json: { error: 'Client Not Found. Check Provided Client Id.' }, 35 | status: :unauthorized 36 | end 37 | allowed_params = user_params.except(:client_id, :client_secret) 38 | 39 | # If email or password are not provided, use existing email and password 40 | allowed_params[:email] = user.email if allowed_params[:email].blank? 41 | allowed_params[:password] = user.password if allowed_params[:password].blank? 42 | 43 | if user.update_with_password(allowed_params) 44 | render json: render_user(user, client_app), status: :ok 45 | else 46 | render json: { errors: user.errors.full_messages }, status: :unprocessable_entity 47 | end 48 | end 49 | 50 | private 51 | 52 | def user_params 53 | params.permit(:email, :password, :current_password, :client_id) 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /backend/app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class UsersController < ApiController 6 | before_action :doorkeeper_authorize! 7 | before_action :current_user 8 | respond_to :json 9 | 10 | # GET /me.json 11 | def me 12 | if @current_user.nil? 13 | render json: { error: 'Not Authorized' }, status: :unauthorized 14 | else 15 | render json: { 16 | id: @current_user.id, 17 | email: @current_user.email, 18 | role: @current_user.role, 19 | created_at: @current_user.created_at.iso8601 20 | }, status: :ok 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /backend/app/controllers/api_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApiController < ApplicationController 4 | # equivalent of authenticate_user! on devise, but this one will check the oauth token 5 | # before_action :authenticate_user! 6 | before_action :doorkeeper_authorize! 7 | 8 | # Skip checking CSRF token authenticity for API requests. 9 | skip_before_action :verify_authenticity_token 10 | 11 | # Set response type 12 | respond_to :json 13 | 14 | # helper method to access the current user from the token 15 | def current_user 16 | return unless doorkeeper_token 17 | 18 | @current_user ||= User.find_by(id: doorkeeper_token[:resource_owner_id]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /backend/app/controllers/apidocs_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApidocsController < ApplicationController 4 | include Swagger::Blocks 5 | 6 | swagger_root do 7 | key :openapi, '3.0.0' 8 | 9 | info do 10 | key :version, '1.0.0' 11 | key :title, 'Devise + Doorkeeper Starter' 12 | key :description, 'Devise + Doorkeeper Starter API documentation' 13 | 14 | contact do 15 | key :name, 'Devise + Doorkeeper Starter' 16 | key :url, 'https://devise-doorkeeper-starter.herokuapp.com' 17 | key :email, 'nejdetkadir.550@gmail.com' 18 | end 19 | end 20 | 21 | server do 22 | key :url, "#{Rails.env.development? ? 'localhost:3000' : 'https://devise-doorkeeper-starter.herokuapp.com'}/api/v1/" 23 | key :description, 'Branchsight API' 24 | end 25 | end 26 | 27 | # A list of all classes that have swagger_* declarations. 28 | SWAGGERED_CLASSES = [ 29 | Swagger::Controllers::Users::TokensController, 30 | Swagger::Controllers::Users::RegistrationsController, 31 | Swagger::Responses::Users::TokenResponses, 32 | Swagger::Responses::Users::RegistrationResponses, 33 | Swagger::Responses::ErrorResponse, 34 | Swagger::Inputs::Users::TokenInputs, 35 | Swagger::Inputs::Users::RegistrationInput, 36 | self 37 | ].freeze 38 | 39 | def index 40 | render html: nil, layout: 'swagger' 41 | end 42 | 43 | def data 44 | render json: Swagger::Blocks.build_root_json(SWAGGERED_CLASSES) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /backend/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | end 5 | -------------------------------------------------------------------------------- /backend/app/controllers/books_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class BooksController < ApplicationController 4 | include ApplicationHelper 5 | before_action :set_book, only: %i[show edit update destroy] 6 | before_action :authenticate_user! 7 | before_action :is_admin? 8 | # GET /books or /books.json 9 | def index 10 | @books = Book.all 11 | end 12 | 13 | # GET /books/1 or /books/1.json 14 | def show; end 15 | 16 | # GET /books/new 17 | def new 18 | @book = Book.new 19 | end 20 | 21 | # GET /books/1/edit 22 | def edit; end 23 | 24 | # POST /books or /books.json 25 | def create 26 | @book = Book.new(book_params) 27 | 28 | respond_to do |format| 29 | if @book.save 30 | format.html { redirect_to book_url(@book), notice: 'Book was successfully created.' } 31 | format.json { render :show, status: :created, location: @book } 32 | else 33 | format.html { render :new, status: :unprocessable_entity } 34 | format.json { render json: @book.errors, status: :unprocessable_entity } 35 | end 36 | end 37 | end 38 | 39 | # PATCH/PUT /books/1 or /books/1.json 40 | def update 41 | respond_to do |format| 42 | if @book.update(book_params) 43 | format.html { redirect_to book_url(@book), notice: 'Book was successfully updated.' } 44 | format.json { render :show, status: :ok, location: @book } 45 | else 46 | format.html { render :edit, status: :unprocessable_entity } 47 | format.json { render json: @book.errors, status: :unprocessable_entity } 48 | end 49 | end 50 | end 51 | 52 | # DELETE /books/1 or /books/1.json 53 | def destroy 54 | @book.destroy 55 | 56 | respond_to do |format| 57 | format.html { redirect_to books_url, notice: 'Book was successfully destroyed.' } 58 | format.json { head :no_content } 59 | end 60 | end 61 | 62 | private 63 | 64 | # Use callbacks to share common setup or constraints between actions. 65 | def set_book 66 | @book = Book.find(params[:id]) 67 | end 68 | 69 | # Only allow a list of trusted parameters through. 70 | def book_params 71 | params.require(:book).permit(:title, :body) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /backend/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /backend/app/controllers/concerns/doorkeeper_registerable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DoorkeeperRegisterable 4 | extend ActiveSupport::Concern 5 | 6 | def generate_refresh_token 7 | loop do 8 | # generate a random token string and return it 9 | # unless there is already another token with the same string 10 | token = SecureRandom.hex(32) 11 | break token unless Doorkeeper::AccessToken.exists?(refresh_token: token) 12 | end 13 | end 14 | 15 | def render_user(user, client_app, token_type = 'Bearer') 16 | access_token = Doorkeeper::AccessToken.create(resource_owner_id: user.id, 17 | application_id: client_app.id, 18 | refresh_token: generate_refresh_token, 19 | expires_in: Doorkeeper.configuration.access_token_expires_in.to_i, 20 | scopes: '') 21 | 22 | { 23 | id: user.id, 24 | email: user.email, 25 | role: user.role, 26 | access_token: access_token.token, 27 | token_type: token_type, 28 | expires_in: access_token.expires_in, 29 | refresh_token: access_token.refresh_token, 30 | created_at: user.created_at.iso8601 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /backend/app/controllers/doorkeeper/application_metal_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class ApplicationMetalController < 5 | Doorkeeper.config.resolve_controller(:base_metal) 6 | include Helpers::Controller 7 | 8 | before_action :enforce_content_type, 9 | if: -> { Doorkeeper.config.enforce_content_type } 10 | 11 | ActiveSupport.run_load_hooks(:doorkeeper_metal_controller, self) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /backend/app/controllers/doorkeeper/tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class TokensController < Doorkeeper::ApplicationMetalController 5 | before_action :validate_presence_of_client, only: [:revoke] 6 | 7 | def create 8 | headers.merge!(authorize_response.headers) 9 | 10 | render json: authorize_response.body, 11 | status: authorize_response.status 12 | rescue Errors::DoorkeeperError => e 13 | handle_token_exception(e) 14 | end 15 | 16 | # OAuth 2.0 Token Revocation - https://datatracker.ietf.org/doc/html/rfc7009 17 | def revoke 18 | # The authorization server responds with HTTP status code 200 if the client 19 | # submitted an invalid token or the token has been revoked successfully. 20 | if token.blank? 21 | render json: {}, status: 200 22 | # The authorization server validates [...] and whether the token 23 | # was issued to the client making the revocation request. If this 24 | # validation fails, the request is refused and the client is informed 25 | # of the error by the authorization server as described below. 26 | elsif authorized? 27 | revoke_token 28 | render json: {}, status: 200 29 | else 30 | render json: revocation_error_response, status: :forbidden 31 | end 32 | end 33 | 34 | def introspect 35 | introspection = OAuth::TokenIntrospection.new(server, token) 36 | 37 | if introspection.authorized? 38 | render json: introspection.to_json, status: 200 39 | else 40 | error = introspection.error_response 41 | headers.merge!(error.headers) 42 | render json: error.body, status: error.status 43 | end 44 | end 45 | 46 | private 47 | 48 | def validate_presence_of_client 49 | return if Doorkeeper.config.skip_client_authentication_for_password_grant 50 | 51 | # @see 2.1. Revocation Request 52 | # 53 | # The client constructs the request by including the following 54 | # parameters using the "application/x-www-form-urlencoded" format in 55 | # the HTTP request entity-body: 56 | # token REQUIRED. 57 | # token_type_hint OPTIONAL. 58 | # 59 | # The client also includes its authentication credentials as described 60 | # in Section 2.3. of [RFC6749]. 61 | # 62 | # The authorization server first validates the client credentials (in 63 | # case of a confidential client) and then verifies whether the token 64 | # was issued to the client making the revocation request. 65 | return if server.client 66 | 67 | # If this validation [client credentials / token ownership] fails, the request is 68 | # refused and the client is informed of the error by the authorization server as 69 | # described below. 70 | # 71 | # @see 2.2.1. Error Response 72 | # 73 | # The error presentation conforms to the definition in Section 5.2 of [RFC6749]. 74 | render json: revocation_error_response, status: :forbidden 75 | end 76 | 77 | # OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential". 78 | # 79 | # RFC7009 80 | # Section 5. Security Considerations 81 | # A malicious client may attempt to guess valid tokens on this endpoint 82 | # by making revocation requests against potential token strings. 83 | # According to this specification, a client's request must contain a 84 | # valid client_id, in the case of a public client, or valid client 85 | # credentials, in the case of a confidential client. The token being 86 | # revoked must also belong to the requesting client. 87 | # 88 | # Once a confidential client is authenticated, it must be authorized to 89 | # revoke the provided access or refresh token. This ensures one client 90 | # cannot revoke another's tokens. 91 | # 92 | # Doorkeeper determines the client type implicitly via the presence of the 93 | # OAuth client associated with a given access or refresh token. Since public 94 | # clients authenticate the resource owner via "password" or "implicit" grant 95 | # types, they set the application_id as null (since the claim cannot be 96 | # verified). 97 | # 98 | # https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 99 | # https://datatracker.ietf.org/doc/html/rfc7009 100 | def authorized? 101 | # Token belongs to specific client, so we need to check if 102 | # authenticated client could access it. 103 | if token.application_id? && token.application.confidential? 104 | # We authorize client by checking token's application 105 | server.client && server.client.application == token.application 106 | else 107 | # Token was issued without client, authorization unnecessary 108 | true 109 | end 110 | end 111 | 112 | def revoke_token 113 | # The authorization server responds with HTTP status code 200 if the token 114 | # has been revoked successfully or if the client submitted an invalid 115 | # token 116 | token.revoke if token&.accessible? 117 | end 118 | 119 | # Doorkeeper does not use the token_type_hint logic described in the 120 | # RFC 7009 due to the refresh token implementation that is a field in 121 | # the access token model. 122 | def token 123 | @token ||= Doorkeeper.config.access_token_model.by_token(params['token']) || 124 | Doorkeeper.config.access_token_model.by_refresh_token(params['token']) 125 | end 126 | 127 | def strategy 128 | @strategy ||= server.token_request(params[:grant_type]) 129 | end 130 | 131 | def authorize_response 132 | @authorize_response ||= begin 133 | before_successful_authorization 134 | auth = strategy.authorize 135 | context = build_context(auth: auth) 136 | after_successful_authorization(context) unless auth.is_a?(Doorkeeper::OAuth::ErrorResponse) 137 | auth 138 | end 139 | end 140 | 141 | def build_context(**attributes) 142 | Doorkeeper::OAuth::Hooks::Context.new(**attributes) 143 | end 144 | 145 | def before_successful_authorization(context = nil) 146 | Doorkeeper.config.before_successful_authorization.call(self, context) 147 | end 148 | 149 | def after_successful_authorization(context) 150 | Doorkeeper.config.after_successful_authorization.call(self, context) 151 | end 152 | 153 | def revocation_error_response 154 | error_description = I18n.t(:unauthorized, scope: %i[doorkeeper errors messages revoke]) 155 | 156 | { error: :unauthorized_client, error_description: error_description } 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /backend/app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | def home 5 | @application = Doorkeeper::Application.find_by(name: 'React Client') 6 | 7 | @application = { 8 | name: @application.name, 9 | client_id: @application.uid, 10 | client_secret: @application.secret 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/controllers/users/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Controllers 5 | module Users 6 | class RegistrationsController 7 | include Swagger::Blocks 8 | 9 | swagger_path '/users' do 10 | operation :post do 11 | key :summary, 'Register' 12 | key :description, 'Register and generate access & refresh tokens' 13 | key :operationId, 'userRegister' 14 | key :tags, [ 15 | 'user' 16 | ] 17 | 18 | request_body do 19 | key :required, true 20 | content :'application/json' do 21 | schema do 22 | key :'$ref', :UserRegistrationInput 23 | end 24 | end 25 | end 26 | 27 | response 200 do 28 | key :description, 'Successfull response' 29 | content :'application/json' do 30 | schema do 31 | key :'$ref', :UserRegistrationSuccessResponse 32 | end 33 | end 34 | end 35 | 36 | response 422 do 37 | key :description, 'Error response' 38 | content :'application/json' do 39 | schema do 40 | key :'$ref', :ErrorResponse 41 | end 42 | end 43 | end 44 | 45 | response 401 do 46 | key :description, 'Invalid client response' 47 | content :'application/json' do 48 | schema do 49 | key :'$ref', :UserRegistrationInvalidClientResponse 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/controllers/users/tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Controllers 5 | module Users 6 | class TokensController 7 | include Swagger::Blocks 8 | 9 | swagger_path '/oauth/token' do 10 | operation :post do 11 | key :summary, 'Login' 12 | key :description, 'Login and generate access & refresh tokens' 13 | key :operationId, 'userLogin' 14 | key :tags, [ 15 | 'user' 16 | ] 17 | 18 | request_body do 19 | key :required, true 20 | content :'application/json' do 21 | schema do 22 | key :'$ref', :UserTokenInput 23 | end 24 | end 25 | end 26 | 27 | response 200 do 28 | key :description, 'Successfull response' 29 | content :'application/json' do 30 | schema do 31 | key :'$ref', :UserTokenSuccessResponse 32 | end 33 | end 34 | end 35 | 36 | response 401 do 37 | key :description, 'Error response' 38 | content :'application/json' do 39 | schema do 40 | key :'$ref', :UserTokenErrorResponse 41 | end 42 | end 43 | end 44 | end 45 | end 46 | 47 | swagger_path '/oauth/revoke' do 48 | operation :post do 49 | key :summary, 'Logout' 50 | key :description, 'Logout and revoke tokens' 51 | key :operationId, 'userLogout' 52 | key :tags, [ 53 | 'user' 54 | ] 55 | 56 | request_body do 57 | key :required, true 58 | content :'application/json' do 59 | schema do 60 | key :'$ref', :UserLogoutInput 61 | end 62 | end 63 | end 64 | 65 | response 200 do 66 | key :description, 'This endpoint returns 200 for every requests' 67 | end 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/inputs/users/registration_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Inputs 5 | module Users 6 | class RegistrationInput 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserRegistrationInput do 11 | key :required, %i[email password client_id] 12 | 13 | property :email do 14 | key :type, :string 15 | end 16 | 17 | property :password do 18 | key :type, :string 19 | end 20 | 21 | property :client_id do 22 | key :type, :string 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/inputs/users/token_inputs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Inputs 5 | module Users 6 | class TokenInputs 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserTokenInput do 11 | key :required, %i[grant_type email password client_id client_secret] 12 | 13 | property :grant_type do 14 | key :type, :string 15 | key :example, 'password' 16 | end 17 | 18 | property :email do 19 | key :type, :string 20 | end 21 | 22 | property :password do 23 | key :type, :string 24 | end 25 | 26 | property :client_id do 27 | key :type, :string 28 | end 29 | 30 | property :client_secret do 31 | key :type, :string 32 | end 33 | end 34 | 35 | schema :UserLogoutInput do 36 | key :required, %i[token client_id client_secret] 37 | 38 | property :token do 39 | key :type, :string 40 | key :example, 'access token or refresh token' 41 | end 42 | 43 | property :client_id do 44 | key :type, :string 45 | end 46 | 47 | property :client_secret do 48 | key :type, :string 49 | end 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/responses/error_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Responses 5 | class ErrorResponse 6 | include Swagger::Blocks 7 | 8 | swagger_component do 9 | schema :ErrorResponse do 10 | property :errors do 11 | key :type, :object 12 | property :email do 13 | key :type, :array 14 | items do 15 | key :type, :string 16 | key :example, 'can not be blank' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/responses/users/registration_responses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Responses 5 | module Users 6 | class RegistrationResponses 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserRegistrationSuccessResponse do 11 | key :type, :object 12 | key :required, %i[id access_token token_type expires_in refresh_token created_at] 13 | 14 | property :id do 15 | key :type, :string 16 | key :format, :uuid 17 | end 18 | 19 | property :access_token do 20 | key :type, :string 21 | end 22 | 23 | property :token_type do 24 | key :type, :string 25 | end 26 | 27 | property :expires_in do 28 | key :type, :integer 29 | end 30 | 31 | property :refresh_token do 32 | key :type, :string 33 | end 34 | 35 | property :created_at do 36 | key :type, :integer 37 | end 38 | end 39 | 40 | schema :UserRegistrationInvalidClientResponse do 41 | key :type, :object 42 | key :required, %i[error error_description] 43 | 44 | property :error do 45 | key :type, :string 46 | end 47 | 48 | property :error_description do 49 | key :type, :string 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /backend/app/controllers/swagger/responses/users/token_responses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Swagger 4 | module Responses 5 | module Users 6 | class TokenResponses 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserTokenSuccessResponse do 11 | key :type, :object 12 | key :required, %i[access_token token_type expires_in refresh_token created_at] 13 | 14 | property :access_token do 15 | key :type, :string 16 | end 17 | 18 | property :token_type do 19 | key :type, :string 20 | end 21 | 22 | property :expires_in do 23 | key :type, :integer 24 | end 25 | 26 | property :refresh_token do 27 | key :type, :string 28 | end 29 | 30 | property :created_at do 31 | key :type, :integer 32 | end 33 | end 34 | 35 | schema :UserTokenErrorResponse do 36 | key :type, :object 37 | key :required, %i[error error_description] 38 | 39 | property :error do 40 | key :type, :string 41 | end 42 | 43 | property :error_description do 44 | key :type, :string 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /backend/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | def is_admin? 5 | return if current_user&.admin? 6 | 7 | respond_to do |format| 8 | format.json { render json: { error: 'You are not authorized to access this page.' }, status: :unauthorized } 9 | format.html { redirect_to root_path, notice: 'You are not authorized to access this page.' } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /backend/app/helpers/books_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BooksHelper 4 | end 5 | -------------------------------------------------------------------------------- /backend/app/helpers/pages_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PagesHelper 4 | end 5 | -------------------------------------------------------------------------------- /backend/app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /backend/app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /backend/app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap under controllers/* 2 | 3 | import { application } from "controllers/application" 4 | 5 | // Eager load all controllers defined in the import map under controllers/**/*_controller 6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 7 | eagerLoadControllersFrom("controllers", application) 8 | 9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) 10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" 11 | // lazyLoadControllersFrom("controllers", application) 12 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | primary_abstract_class 5 | end 6 | -------------------------------------------------------------------------------- /backend/app/models/book.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Book < ApplicationRecord 4 | end 5 | -------------------------------------------------------------------------------- /backend/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/app/models/concerns/.keep -------------------------------------------------------------------------------- /backend/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 | validates :email, format: URI::MailTo::EMAIL_REGEXP 10 | enum role: %i[user admin] 11 | 12 | # the authenticate method from devise documentation 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 | -------------------------------------------------------------------------------- /backend/app/views/books/_book.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | Title: 4 | <%= book.title %> 5 |

6 | 7 |

8 | Body: 9 | <%= book.body %> 10 |

11 | 12 |
13 | -------------------------------------------------------------------------------- /backend/app/views/books/_book.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.extract! book, :id, :title, :body, :created_at, :updated_at 4 | json.url book_url(book, format: :json) 5 | -------------------------------------------------------------------------------- /backend/app/views/books/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with(model: book) do |form| %> 2 | <% if book.errors.any? %> 3 |
4 |

<%= pluralize(book.errors.count, "error") %> prohibited this book from being saved:

5 | 6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= form.label :title, style: "display: block" %> 16 | <%= form.text_field :title %> 17 |
18 | 19 |
20 | <%= form.label :body, style: "display: block" %> 21 | <%= form.text_area :body %> 22 |
23 | 24 |
25 | <%= form.submit %> 26 |
27 | <% end %> 28 | -------------------------------------------------------------------------------- /backend/app/views/books/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing book

2 | 3 | <%= render "form", book: @book %> 4 | 5 |
6 | 7 |
8 | <%= link_to "Show this book", @book %> | 9 | <%= link_to "Back to books", books_path %> 10 |
11 | -------------------------------------------------------------------------------- /backend/app/views/books/index.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 |

Books

3 |

4 | <%= "#{current_user.email}"%> 5 |

6 |

<%= "Role: #{current_user.role}" %>

7 |
8 | <% @books.each do |book| %> 9 | <%= render book %> 10 |

11 | <%= link_to "Show this book", book %> 12 |

13 | <% end %> 14 |
15 | <%= link_to "New book", new_book_path %> 16 | -------------------------------------------------------------------------------- /backend/app/views/books/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.array! @books, partial: 'books/book', as: :book 4 | -------------------------------------------------------------------------------- /backend/app/views/books/new.html.erb: -------------------------------------------------------------------------------- 1 |

New book

2 | 3 | <%= render "form", book: @book %> 4 | 5 |
6 | 7 |
8 | <%= link_to "Back to books", books_path %> 9 |
10 | -------------------------------------------------------------------------------- /backend/app/views/books/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 | 3 | <%= render @book %> 4 | 5 |
6 | <%= link_to "Edit this book", edit_book_path(@book) %> | 7 | <%= link_to "Back to books", books_path %> 8 | 9 | <%= button_to "Destroy this book", @book, method: :delete %> 10 |
11 | -------------------------------------------------------------------------------- /backend/app/views/books/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.partial! 'books/book', book: @book 4 | -------------------------------------------------------------------------------- /backend/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rails Web Application 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | 10 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> 11 | <%= javascript_importmap_tags %> 12 | 13 | 14 |
15 |
16 | <%= yield %> 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /backend/app/views/layouts/swagger.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI 5 | 6 | 27 | 28 | 29 |
30 | 31 | 32 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /backend/app/views/pages/home.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_user %> 2 |

<%= "Hello, #{current_user.email}" %>

3 | <% end %> 4 |

Welcome to the Bookstore's Rails Portal!

5 |

Find me in app/views/pages/home.html.erb

6 | <% if user_signed_in? %> 7 | <%= button_to "Sign out", destroy_user_session_path, method: :delete %> 8 | <% else %> 9 | <%= link_to "Sign in", new_user_session_path %> 10 | <%= link_to "Sign up", new_user_registration_path %> 11 | <% end %> 12 |

For DoorkeeperAPI documentation, check <%= link_to "here", swagger_root_path %>

13 |

For the Bookstore API documentation, check <%= link_to "here", rswag_ui_path %>

14 |

You can test the API with following credentials.

15 |
16 | <%= @application.as_json %> 17 | 22 |
23 | -------------------------------------------------------------------------------- /backend/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($PROGRAM_NAME) == 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 | 28 | bundler_version = nil 29 | update_index = nil 30 | ARGV.each_with_index do |a, i| 31 | bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 32 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 33 | 34 | bundler_version = Regexp.last_match(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', __dir__) 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 | 59 | lockfile_contents = File.read(lockfile) 60 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 61 | 62 | Regexp.last_match(1) 63 | end 64 | 65 | def bundler_requirement 66 | @bundler_requirement ||= 67 | env_var_version || cli_arg_version || 68 | bundler_requirement_for(lockfile_version) 69 | end 70 | 71 | def bundler_requirement_for(version) 72 | return "#{Gem::Requirement.default}.a" unless version 73 | 74 | bundler_gem_version = Gem::Version.new(version) 75 | 76 | requirement = bundler_gem_version.approximate_recommendation 77 | 78 | return requirement unless Gem.rubygems_version < Gem::Version.new('2.7.0') 79 | 80 | requirement += '.a' if bundler_gem_version.prerelease? 81 | 82 | requirement 83 | end 84 | 85 | def load_bundler! 86 | ENV['BUNDLE_GEMFILE'] ||= gemfile 87 | 88 | activate_bundler 89 | end 90 | 91 | def activate_bundler 92 | gem_error = activation_error_handling do 93 | gem 'bundler', bundler_requirement 94 | end 95 | return if gem_error.nil? 96 | 97 | require_error = activation_error_handling do 98 | require 'bundler/version' 99 | end 100 | if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 101 | return 102 | end 103 | 104 | 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}'`" 105 | exit 42 106 | end 107 | 108 | def activation_error_handling 109 | yield 110 | nil 111 | rescue StandardError, LoadError => e 112 | e 113 | end 114 | end 115 | 116 | m.load_bundler! 117 | 118 | load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? 119 | -------------------------------------------------------------------------------- /backend/bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../config/application' 5 | require 'importmap/commands' 6 | -------------------------------------------------------------------------------- /backend/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_PATH = File.expand_path('../config/application', __dir__) 5 | require_relative '../config/boot' 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /backend/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative '../config/boot' 5 | require 'rake' 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /backend/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'fileutils' 5 | 6 | # path to your application root. 7 | APP_ROOT = File.expand_path('..', __dir__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | FileUtils.chdir APP_ROOT do 14 | # This script is a way to set up or update your development environment automatically. 15 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 16 | # Add necessary setup steps to this file. 17 | 18 | puts '== Installing dependencies ==' 19 | system! 'gem install bundler --conservative' 20 | system('bundle check') || system!('bundle install') 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?("config/database.yml") 24 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! 'bin/rails db:prepare' 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! 'bin/rails log:clear tmp:clear' 32 | 33 | puts "\n== Restarting application server ==" 34 | system! 'bin/rails restart' 35 | end 36 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails/all' 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | 11 | module DoorkeeperApi 12 | class Application < Rails::Application 13 | # Initialize configuration defaults for originally generated Rails version. 14 | config.load_defaults 7.0 15 | 16 | # Configuration for the application, engines, and railties goes here. 17 | # 18 | # These settings can be overridden in specific environments using the files 19 | # in config/environments, which are processed later. 20 | # 21 | # config.time_zone = "Central Time (US & Canada)" 22 | # config.eager_load_paths << Rails.root.join("extras") 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /backend/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 4 | 5 | require 'bundler/setup' # Set up gems listed in the Gemfile. 6 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 7 | -------------------------------------------------------------------------------- /backend/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: doorkeeper_api_production 12 | -------------------------------------------------------------------------------- /backend/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | br1d9UPFQJ/HQXdI9nFZWT+h6t7UuZ75tAJsX0CXL693Pkj3ed+CpkPogMBvOV01MtcZw8/1CLJxz0sOY0CrvMMrW8N3nj1wzjPLbnyfzN7izKhdP8KFghcbZ7XwWpsUBNQ+WLqVpq9g1Z3qgpozPxi75wypxEDjMK9QCZmVc07XhqLTWuhbnz+c2KdcUWTkp8aLtvTJHTs2OxadawfzPMRPmer6x7uIX4LFm8oBRL4n8sqovmoJULFbic5Q5ssu1fMJa4EbW34G0V+V7jJ/4c4IEfKrEaXgD0/lGaJiEnzGzzr+49pK2NkZDOf+LCiYWD66ojJNKDw4WkRL3HzFWBbFeSEwTIuxkn1dyxqALUZN/I+xRp8HOojHj4IGRSQpcXn/i5TCAXWifCjNOxkMzj8VUIPiBr218Ux9--Tjfns7hNArT+kdCP--bTL38QlyQPu0hFbhjn9tRg== -------------------------------------------------------------------------------- /backend/config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.3 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On macOS with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On macOS with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem "pg" 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see Rails configuration guide 21 | # https://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: backend_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user running Rails. 32 | #username: backend 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: backend_test 61 | 62 | # As with config/credentials.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password or a full connection URL as an environment 67 | # variable when you boot the app. For example: 68 | # 69 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 70 | # 71 | # If the connection URL is provided in the special DATABASE_URL environment 72 | # variable, Rails will automatically merge its configuration values on top of 73 | # the values provided in this file. Alternatively, you can specify a connection 74 | # URL environment variable explicitly: 75 | # 76 | # production: 77 | # url: <%= ENV["MY_APP_DATABASE_URL"] %> 78 | # 79 | # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database 80 | # for a full overview on how database connection configuration can be specified. 81 | # 82 | production: 83 | <<: *default 84 | database: backend_production 85 | username: backend 86 | password: <%= ENV["BACKEND_DATABASE_PASSWORD"] %> 87 | -------------------------------------------------------------------------------- /backend/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /backend/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # Used to set an HTTP only cookie session. 9 | config.session_store :cookie_store, 10 | key: 'session', 11 | domain: :all 12 | # In the development environment your application's code is reloaded any time 13 | # it changes. This slows down response time but is perfect for development 14 | # since you don't have to restart the web server when you make code changes. 15 | config.cache_classes = false 16 | 17 | # Do not eager load code on boot. 18 | config.eager_load = false 19 | 20 | # Show full error reports. 21 | config.consider_all_requests_local = true 22 | 23 | # Enable server timing 24 | config.server_timing = true 25 | 26 | # Enable/disable caching. By default caching is disabled. 27 | # Run rails dev:cache to toggle caching. 28 | if Rails.root.join('tmp/caching-dev.txt').exist? 29 | config.action_controller.perform_caching = true 30 | config.action_controller.enable_fragment_cache_logging = true 31 | 32 | config.cache_store = :memory_store 33 | config.public_file_server.headers = { 34 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 35 | } 36 | else 37 | config.action_controller.perform_caching = false 38 | 39 | config.cache_store = :null_store 40 | end 41 | 42 | # Store uploaded files on the local file system (see config/storage.yml for options). 43 | config.active_storage.service = :local 44 | 45 | # Don't care if the mailer can't send. 46 | config.action_mailer.raise_delivery_errors = false 47 | 48 | config.action_mailer.perform_caching = false 49 | 50 | # Print deprecation notices to the Rails logger. 51 | config.active_support.deprecation = :log 52 | 53 | # Raise exceptions for disallowed deprecations. 54 | config.active_support.disallowed_deprecation = :raise 55 | 56 | # Tell Active Support which deprecation messages to disallow. 57 | config.active_support.disallowed_deprecation_warnings = [] 58 | 59 | # Raise an error on page load if there are pending migrations. 60 | config.active_record.migration_error = :page_load 61 | 62 | # Highlight code that triggered database queries in logs. 63 | config.active_record.verbose_query_logs = true 64 | 65 | # Suppress logger output for asset requests. 66 | config.assets.quiet = true 67 | 68 | # Raises error for missing translations. 69 | # config.i18n.raise_on_missing_translations = true 70 | 71 | # Annotate rendered view with file names. 72 | # config.action_view.annotate_rendered_view_with_filenames = true 73 | 74 | # Uncomment if you wish to allow Action Cable access from any origin. 75 | # config.action_cable.disable_request_forgery_protection = true 76 | end 77 | -------------------------------------------------------------------------------- /backend/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # Code is not reloaded between requests. 9 | config.cache_classes = true 10 | 11 | # Eager load code on boot. This eager loads most of Rails and 12 | # your application in memory, allowing both threaded web servers 13 | # and those relying on copy on write to perform better. 14 | # Rake tasks automatically ignore this option for performance. 15 | config.eager_load = true 16 | 17 | # Full error reports are disabled and caching is turned on. 18 | config.consider_all_requests_local = false 19 | config.action_controller.perform_caching = true 20 | 21 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 22 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 23 | # config.require_master_key = true 24 | 25 | # Disable serving static files from the `/public` folder by default since 26 | # Apache or NGINX already handles this. 27 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 28 | 29 | # Compress CSS using a preprocessor. 30 | # config.assets.css_compressor = :sass 31 | 32 | # Do not fallback to assets pipeline if a precompiled asset is missed. 33 | config.assets.compile = false 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.asset_host = "http://assets.example.com" 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 40 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 41 | 42 | # Store uploaded files on the local file system (see config/storage.yml for options). 43 | config.active_storage.service = :local 44 | 45 | # Mount Action Cable outside main process or domain. 46 | # config.action_cable.mount_path = nil 47 | # config.action_cable.url = "wss://example.com/cable" 48 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 49 | 50 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 51 | # config.force_ssl = true 52 | 53 | # Include generic and useful information about system operation, but avoid logging too much 54 | # information to avoid inadvertent exposure of personally identifiable information (PII). 55 | config.log_level = :info 56 | 57 | # Prepend all log lines with the following tags. 58 | config.log_tags = [:request_id] 59 | 60 | # Use a different cache store in production. 61 | # config.cache_store = :mem_cache_store 62 | 63 | # Use a real queuing backend for Active Job (and separate queues per environment). 64 | # config.active_job.queue_adapter = :resque 65 | # config.active_job.queue_name_prefix = "doorkeeper_api_production" 66 | 67 | config.action_mailer.perform_caching = false 68 | 69 | # Ignore bad email addresses and do not raise email delivery errors. 70 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 71 | # config.action_mailer.raise_delivery_errors = false 72 | 73 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 74 | # the I18n.default_locale when a translation cannot be found). 75 | config.i18n.fallbacks = true 76 | 77 | # Don't log any deprecations. 78 | config.active_support.report_deprecations = false 79 | 80 | # Use default logging formatter so that PID and timestamp are not suppressed. 81 | config.log_formatter = ::Logger::Formatter.new 82 | 83 | # Use a different logger for distributed setups. 84 | # require "syslog/logger" 85 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 86 | 87 | if ENV['RAILS_LOG_TO_STDOUT'].present? 88 | logger = ActiveSupport::Logger.new($stdout) 89 | logger.formatter = config.log_formatter 90 | config.logger = ActiveSupport::TaggedLogging.new(logger) 91 | end 92 | 93 | # Do not dump schema after migrations. 94 | config.active_record.dump_schema_after_migration = false 95 | end 96 | -------------------------------------------------------------------------------- /backend/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | 10 | Rails.application.configure do 11 | # Settings specified here will take precedence over those in config/application.rb. 12 | 13 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 14 | config.cache_classes = true 15 | 16 | # Eager loading loads your whole application. When running a single test locally, 17 | # this probably isn't necessary. It's a good idea to do in a continuous integration 18 | # system, or in some way before deploying your code. 19 | config.eager_load = ENV['CI'].present? 20 | 21 | # Configure public file server for tests with Cache-Control for performance. 22 | config.public_file_server.enabled = true 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 25 | } 26 | 27 | # Show full error reports and disable caching. 28 | config.consider_all_requests_local = true 29 | config.action_controller.perform_caching = false 30 | config.cache_store = :null_store 31 | 32 | # Raise exceptions instead of rendering exception templates. 33 | config.action_dispatch.show_exceptions = false 34 | 35 | # Disable request forgery protection in test environment. 36 | config.action_controller.allow_forgery_protection = false 37 | 38 | # Store uploaded files on the local file system in a temporary directory. 39 | config.active_storage.service = :test 40 | 41 | config.action_mailer.perform_caching = false 42 | 43 | # Tell Action Mailer not to deliver emails to the real world. 44 | # The :test delivery method accumulates sent emails in the 45 | # ActionMailer::Base.deliveries array. 46 | config.action_mailer.delivery_method = :test 47 | 48 | # Print deprecation notices to the stderr. 49 | config.active_support.deprecation = :stderr 50 | 51 | # Raise exceptions for disallowed deprecations. 52 | config.active_support.disallowed_deprecation = :raise 53 | 54 | # Tell Active Support which deprecation messages to disallow. 55 | config.active_support.disallowed_deprecation_warnings = [] 56 | 57 | # Raises error for missing translations. 58 | # config.i18n.raise_on_missing_translations = true 59 | 60 | # Annotate rendered view with file names. 61 | # config.action_view.annotate_rendered_view_with_filenames = true 62 | end 63 | -------------------------------------------------------------------------------- /backend/config/importmap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Pin npm packages by running ./bin/importmap 4 | 5 | pin 'application', preload: true 6 | pin '@hotwired/turbo-rails', to: 'turbo.min.js', preload: true 7 | pin '@hotwired/stimulus', to: 'stimulus.min.js', preload: true 8 | pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js', preload: true 9 | pin_all_from 'app/javascript/controllers', under: 'controllers' 10 | -------------------------------------------------------------------------------- /backend/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = '1.0' 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /backend/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Define an application-wide content security policy 5 | # For further information see the following documentation 6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 7 | 8 | # Rails.application.configure do 9 | # config.content_security_policy do |policy| 10 | # policy.default_src :self, :https 11 | # policy.font_src :self, :https, :data 12 | # policy.img_src :self, :https, :data 13 | # policy.object_src :none 14 | # policy.script_src :self, :https 15 | # policy.style_src :self, :https 16 | # # Specify URI for violation reports 17 | # # policy.report_uri "/csp-violation-report-endpoint" 18 | # end 19 | # 20 | # # Generate session nonces for permitted importmap and inline scripts 21 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 22 | # config.content_security_policy_nonce_directives = %w(script-src) 23 | # 24 | # # Report CSP violations to a specified URI. See: 25 | # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 26 | # # config.content_security_policy_report_only = true 27 | # end 28 | -------------------------------------------------------------------------------- /backend/config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 4 | allow do 5 | origins '*' 6 | resource '/api/v1/*', 7 | headers: :any, 8 | methods: %i[get post patch put delete] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /backend/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 6 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 7 | # notations and behaviors. 8 | Rails.application.config.filter_parameters += %i[ 9 | passw secret token _key crypt salt certificate otp ssn 10 | ] 11 | -------------------------------------------------------------------------------- /backend/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, "\\1en" 9 | # inflect.singular /^(ox)en/i, "\\1" 10 | # inflect.irregular "person", "people" 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym "RESTful" 17 | # end 18 | -------------------------------------------------------------------------------- /backend/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define an application-wide HTTP permissions policy. For further 3 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 4 | # 5 | # Rails.application.config.permissions_policy do |f| 6 | # f.camera :none 7 | # f.gyroscope :none 8 | # f.microphone :none 9 | # f.usb :none 10 | # f.fullscreen :self 11 | # f.payment :self, "https://secure.example.com" 12 | # end 13 | -------------------------------------------------------------------------------- /backend/config/initializers/rswag-ui.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rswag::Ui.configure do |c| 4 | # List the Swagger endpoints that you want to be documented through the swagger-ui 5 | # The first parameter is the path (absolute or relative to the UI host) to the corresponding 6 | # endpoint and the second is a title that will be displayed in the document selector 7 | # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints, 8 | # then the list below should correspond to the relative paths for those endpoints 9 | 10 | c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' 11 | 12 | # Add Basic Auth in case your API is private 13 | # c.basic_auth_enabled = true 14 | # c.basic_auth_credentials 'username', 'password' 15 | end 16 | -------------------------------------------------------------------------------- /backend/config/initializers/rswag_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rswag::Api.configure do |c| 4 | # Specify a root folder where Swagger JSON files are located 5 | # This is used by the Swagger middleware to serve requests for API descriptions 6 | # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure 7 | # that it's configured to generate files in the same folder 8 | c.swagger_root = "#{Rails.root}/swagger" 9 | 10 | # Inject a lamda function to alter the returned Swagger prior to serialization 11 | # The function will have access to the rack env for the current request 12 | # For example, you could leverage this to dynamically assign the "host" property 13 | # 14 | # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } 15 | end 16 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 14 | # terminating a worker in development environments. 15 | # 16 | worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' 17 | 18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 19 | # 20 | port ENV.fetch('PORT', 3000) 21 | 22 | # Specifies the `environment` that Puma will run in. 23 | # 24 | environment ENV.fetch('RAILS_ENV', 'development') 25 | 26 | # Specifies the `pidfile` that Puma will use. 27 | pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') 28 | 29 | # Specifies the number of `workers` to boot in clustered mode. 30 | # Workers are forked web server processes. If using threads and workers together 31 | # the concurrency of the application would be max `threads` * `workers`. 32 | # Workers do not work on JRuby or Windows (both of which do not support 33 | # processes). 34 | # 35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 36 | 37 | # Use the `preload_app!` method when specifying a `workers` number. 38 | # This directive tells Puma to first boot the application and load code 39 | # before forking the application. This takes advantage of Copy On Write 40 | # process behavior so workers use less memory. 41 | # 42 | # preload_app! 43 | 44 | # Allow puma to be restarted by `bin/rails restart` command. 45 | plugin :tmp_restart 46 | -------------------------------------------------------------------------------- /backend/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | mount Rswag::Ui::Engine => '/api-docs' 5 | mount Rswag::Api::Engine => '/api-docs' 6 | root 'pages#home' 7 | 8 | use_doorkeeper 9 | devise_for :users 10 | resources :books 11 | 12 | draw :api 13 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 14 | 15 | # Defines the root path route ("/") 16 | # root "articles#index" 17 | end 18 | -------------------------------------------------------------------------------- /backend/config/routes/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :api do 4 | namespace :v1 do 5 | scope :users, module: :users do 6 | post '/', to: 'registrations#create', as: :user_registration 7 | patch '/', to: 'registrations#update_profile', as: :user_update_profile 8 | end 9 | resources :books 10 | 11 | namespace :android do 12 | resources :books 13 | end 14 | get '/users/me', to: 'users#me' 15 | end 16 | end 17 | 18 | scope :api do 19 | scope :v1 do 20 | # Swagger documentation 21 | scope :swagger do 22 | get '/', to: 'apidocs#index', as: :swagger_root 23 | get '/data', to: 'apidocs#data', as: :swagger_data 24 | end 25 | use_doorkeeper do 26 | skip_controllers :authorizations, :applications, :authorized_applications 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/db/migrate/20220404025649_create_books.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateBooks < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :books do |t| 6 | t.string :title 7 | t.text :body 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /backend/db/migrate/20220404025721_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 | t.timestamps null: false 36 | end 37 | 38 | add_index :users, :email, unique: true 39 | add_index :users, :reset_password_token, unique: true 40 | # add_index :users, :confirmation_token, unique: true 41 | # add_index :users, :unlock_token, unique: true 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /backend/db/migrate/20220404025750_add_role_to_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddRoleToUser < ActiveRecord::Migration[7.0] 4 | def change 5 | add_column :users, :role, :integer, default: 0 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /backend/db/migrate/20220404030809_create_doorkeeper_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDoorkeeperTables < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :oauth_applications do |t| 6 | t.string :name, null: false 7 | t.string :uid, null: false 8 | t.string :secret, null: false 9 | 10 | # Remove `null: false` if you are planning to use grant flows 11 | # that doesn't require redirect URI to be used during authorization 12 | # like Client Credentials flow or Resource Owner Password. 13 | t.text :redirect_uri 14 | t.string :scopes, null: false, default: '' 15 | t.boolean :confidential, null: false, default: true 16 | t.timestamps null: false 17 | end 18 | 19 | add_index :oauth_applications, :uid, unique: true 20 | 21 | # create_table :oauth_access_grants do |t| 22 | # t.references :resource_owner, null: false 23 | # t.references :application, null: false 24 | # t.string :token, null: false 25 | # t.integer :expires_in, null: false 26 | # t.text :redirect_uri, null: false 27 | # t.datetime :created_at, null: false 28 | # t.datetime :revoked_at 29 | # t.string :scopes, null: false, default: '' 30 | # end 31 | 32 | # add_index :oauth_access_grants, :token, unique: true 33 | # add_foreign_key( 34 | # :oauth_access_grants, 35 | # :oauth_applications, 36 | # column: :application_id 37 | # ) 38 | 39 | create_table :oauth_access_tokens do |t| 40 | t.references :resource_owner, index: true 41 | 42 | # Remove `null: false` if you are planning to use Password 43 | # Credentials Grant flow that doesn't require an application. 44 | t.references :application, null: false 45 | 46 | # If you use a custom token generator you may need to change this column 47 | # from string to text, so that it accepts tokens larger than 255 48 | # characters. More info on custom token generators in: 49 | # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator 50 | # 51 | # t.text :token, null: false 52 | t.string :token, null: false 53 | 54 | t.string :refresh_token 55 | t.integer :expires_in 56 | t.datetime :revoked_at 57 | t.datetime :created_at, null: false 58 | t.string :scopes 59 | 60 | # The authorization server MAY issue a new refresh token, in which case 61 | # *the client MUST discard the old refresh token* and replace it with the 62 | # new refresh token. The authorization server MAY revoke the old 63 | # refresh token after issuing a new refresh token to the client. 64 | # @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 65 | # 66 | # Doorkeeper implementation: if there is a `previous_refresh_token` column, 67 | # refresh tokens will be revoked after a related access token is used. 68 | # If there is no `previous_refresh_token` column, previous tokens are 69 | # revoked as soon as a new access token is created. 70 | # 71 | # Comment out this line if you want refresh tokens to be instantly 72 | # revoked after use. 73 | t.string :previous_refresh_token, null: false, default: '' 74 | end 75 | 76 | add_index :oauth_access_tokens, :token, unique: true 77 | add_index :oauth_access_tokens, :refresh_token, unique: true 78 | add_foreign_key( 79 | :oauth_access_tokens, 80 | :oauth_applications, 81 | column: :application_id 82 | ) 83 | 84 | # Uncomment below to ensure a valid reference to the resource owner's table 85 | # add_foreign_key :oauth_access_grants, , column: :resource_owner_id 86 | # add_foreign_key :oauth_access_tokens, , column: :resource_owner_id 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /backend/db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is auto-generated from the current state of the database. Instead 4 | # of editing this file, please use the migrations feature of Active Record to 5 | # incrementally modify your database, and then regenerate this schema definition. 6 | # 7 | # This file is the source Rails uses to define your schema when running `bin/rails 8 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 9 | # be faster and is potentially less error prone than running all of your 10 | # migrations from scratch. Old migrations may fail to apply correctly if those 11 | # migrations use external dependencies or application code. 12 | # 13 | # It's strongly recommended that you check this file into your version control system. 14 | 15 | ActiveRecord::Schema[7.0].define(version: 20_220_404_030_809) do 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension 'plpgsql' 18 | 19 | create_table 'books', force: :cascade do |t| 20 | t.string 'title' 21 | t.text 'body' 22 | t.datetime 'created_at', null: false 23 | t.datetime 'updated_at', null: false 24 | end 25 | 26 | create_table 'oauth_access_tokens', force: :cascade do |t| 27 | t.bigint 'resource_owner_id' 28 | t.bigint 'application_id', null: false 29 | t.string 'token', null: false 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.string 'previous_refresh_token', default: '', null: false 36 | t.index ['application_id'], name: 'index_oauth_access_tokens_on_application_id' 37 | t.index ['refresh_token'], name: 'index_oauth_access_tokens_on_refresh_token', unique: true 38 | t.index ['resource_owner_id'], name: 'index_oauth_access_tokens_on_resource_owner_id' 39 | t.index ['token'], name: 'index_oauth_access_tokens_on_token', unique: true 40 | end 41 | 42 | create_table 'oauth_applications', force: :cascade do |t| 43 | t.string 'name', null: false 44 | t.string 'uid', null: false 45 | t.string 'secret', null: false 46 | t.text 'redirect_uri' 47 | t.string 'scopes', default: '', null: false 48 | t.boolean 'confidential', default: true, null: false 49 | t.datetime 'created_at', null: false 50 | t.datetime 'updated_at', null: false 51 | t.index ['uid'], name: 'index_oauth_applications_on_uid', unique: true 52 | end 53 | 54 | create_table 'users', force: :cascade do |t| 55 | t.string 'email', default: '', null: false 56 | t.string 'encrypted_password', default: '', null: false 57 | t.string 'reset_password_token' 58 | t.datetime 'reset_password_sent_at' 59 | t.datetime 'remember_created_at' 60 | t.datetime 'created_at', null: false 61 | t.datetime 'updated_at', null: false 62 | t.integer 'role', default: 0 63 | t.index ['email'], name: 'index_users_on_email', unique: true 64 | t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true 65 | end 66 | 67 | add_foreign_key 'oauth_access_tokens', 'oauth_applications', column: 'application_id' 68 | end 69 | -------------------------------------------------------------------------------- /backend/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file should contain all the record creation needed to seed the database with its default values. 4 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 5 | # 6 | # Examples: 7 | # 8 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 9 | # Character.create(name: "Luke", movie: movies.first) 10 | 11 | load(Rails.root.join('db', 'seeds', "#{Rails.env.downcase}.rb")) 12 | -------------------------------------------------------------------------------- /backend/db/seeds/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if Doorkeeper::Application.count.zero? 4 | Doorkeeper::Application.create!(name: 'React Client', redirect_uri: '', scopes: '') 5 | end 6 | 7 | User.first_or_create(email: 'dean@example.com', 8 | password: 'password', 9 | password_confirmation: 'password', 10 | role: User.roles[:admin]) 11 | -------------------------------------------------------------------------------- /backend/db/seeds/production.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/db/seeds/production.rb -------------------------------------------------------------------------------- /backend/db/seeds/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if Doorkeeper::Application.count.zero? 4 | Doorkeeper::Application.create!(name: 'React Client', redirect_uri: '', scopes: '') 5 | 6 | end 7 | -------------------------------------------------------------------------------- /backend/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/lib/assets/.keep -------------------------------------------------------------------------------- /backend/lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/lib/tasks/.keep -------------------------------------------------------------------------------- /backend/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/log/.keep -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /backend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /backend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/public/favicon.ico -------------------------------------------------------------------------------- /backend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /backend/scripts/rubocop.sh: -------------------------------------------------------------------------------- 1 | bundle exec rubocop --parallel -A -------------------------------------------------------------------------------- /backend/scripts/swagger.sh: -------------------------------------------------------------------------------- 1 | rake rswag:specs:swaggerize -------------------------------------------------------------------------------- /backend/spec/integration/books_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'swagger_helper' 4 | require 'rails_helper' 5 | 6 | # Describe the books API 7 | describe 'Books API' do # rubocop:disable Metrics/BlockLength 8 | before do 9 | @token = "Bearer #{create(:doorkeeper_access_token).token}" 10 | @book = create(:book).attributes 11 | end 12 | # GET /books 13 | # Get all books 14 | path '/api/v1/books' do 15 | get 'Get all books' do 16 | tags 'Books' 17 | security [Bearer: []] 18 | parameter name: :Authorization, in: :header, type: :string, required: true, 19 | description: 'Authorization token' 20 | response '200', 'books found' do 21 | let(:Authorization) { @token } 22 | run_test! 23 | end 24 | response '401', 'unauthorized' do 25 | let(:Authorization) { 'invalid' } 26 | run_test! 27 | end 28 | end 29 | end 30 | 31 | # GET /books/:id 32 | # Get a book by id 33 | path '/api/v1/books/{id}' do 34 | get 'Get a book' do 35 | tags 'Books' 36 | security [Bearer: []] 37 | parameter name: :Authorization, in: :header, type: :string, required: true, 38 | description: 'Authorization token' 39 | parameter name: :id, in: :path, type: :string, required: true, 40 | description: 'ID of the book' 41 | response '200', 'book found' do 42 | let(:Authorization) { @token } 43 | let(:id) { @book['id'] } 44 | run_test! 45 | end 46 | response '404', 'book not found' do 47 | let(:Authorization) { @token } 48 | let(:id) { 'invalid' } 49 | run_test! 50 | end 51 | response '401', 'unauthorized' do 52 | let(:Authorization) { 'invalid' } 53 | let(:id) { @book['id'] } 54 | run_test! 55 | end 56 | end 57 | end 58 | 59 | # POST /books 60 | # Create a book 61 | path '/api/v1/books' do 62 | post 'Create a book' do 63 | tags 'Books' 64 | consumes 'application/json', 'application/xml' 65 | security [Bearer: []] 66 | parameter name: :Authorization, in: :header, type: :string, required: true, 67 | description: 'Authorization token' 68 | parameter name: :book, in: :body, schema: { 69 | type: :object, 70 | properties: { 71 | book: { 72 | 73 | title: { type: :string }, 74 | body: { type: :string } 75 | } 76 | }, 77 | required: %w[title body] 78 | } 79 | response '302', 'redirected' do 80 | let(:Authorization) { @token } 81 | let(:book) { { title: 'The Hobbit', body: 'A great book' } } 82 | run_test! 83 | end 84 | response '401', 'unauthorized' do 85 | let(:Authorization) { 'invalid' } 86 | let(:book) { { book: attributes_for(:book) } } 87 | run_test! 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /backend/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 | require 'support/factory_bot' 11 | # Add additional requires below this line. Rails is not loaded until this point! 12 | 13 | # Requires supporting ruby files with custom matchers and macros, etc, in 14 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 15 | # run as spec files by default. This means that files in spec/support that end 16 | # in _spec.rb will both be required and run as specs, causing the specs to be 17 | # run twice. It is recommended that you do not name files matching this glob to 18 | # end with _spec.rb. You can configure this pattern with the --pattern 19 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 20 | # 21 | # The following line is provided for convenience purposes. It has the downside 22 | # of increasing the boot-up time by auto-requiring all files in the support 23 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 24 | # require only the support files necessary. 25 | # 26 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } 27 | 28 | # Checks for pending migrations and applies them before tests are run. 29 | # If you are not using ActiveRecord, you can remove these lines. 30 | begin 31 | ActiveRecord::Migration.maintain_test_schema! 32 | rescue ActiveRecord::PendingMigrationError => e 33 | puts e.to_s.strip 34 | exit 1 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 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 41 | # examples within a transaction, remove the following line or assign false 42 | # instead of true. 43 | config.use_transactional_fixtures = true 44 | 45 | # You can uncomment this line to turn off ActiveRecord support entirely. 46 | # config.use_active_record = false 47 | 48 | # RSpec Rails can automatically mix in different behaviours to your tests 49 | # based on their file location, for example enabling you to call `get` and 50 | # `post` in specs under `spec/controllers`. 51 | # 52 | # You can disable this behaviour by removing the line below, and instead 53 | # explicitly tag your specs with their type, e.g.: 54 | # 55 | # RSpec.describe UsersController, type: :controller do 56 | # # ... 57 | # end 58 | # 59 | # The different available types are documented in the features, such as in 60 | # https://relishapp.com/rspec/rspec-rails/docs 61 | config.infer_spec_type_from_file_location! 62 | 63 | # Filter lines from Rails gems in backtraces. 64 | config.filter_rails_from_backtrace! 65 | # arbitrary gems may also be filtered via: 66 | # config.filter_gems_from_backtrace("gem name") 67 | end 68 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/spec/swagger_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | RSpec.configure do |config| 6 | # Specify a root folder where Swagger JSON files are generated 7 | # NOTE: If you're using the rswag-api to serve API descriptions, you'll need 8 | # to ensure that it's configured to serve Swagger from the same folder 9 | config.swagger_root = Rails.root.join('swagger').to_s 10 | 11 | # Define one or more Swagger documents and provide global metadata for each one 12 | # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will 13 | # be generated at the provided relative path under swagger_root 14 | # By default, the operations defined in spec files are added to the first 15 | # document below. You can override this behavior by adding a swagger_doc tag to the 16 | # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' 17 | config.swagger_docs = { 18 | 'v1/swagger.yaml' => { 19 | openapi: '3.0.1', 20 | info: { 21 | title: 'API V1', 22 | version: 'v1' 23 | }, 24 | paths: {}, 25 | servers: [ 26 | { 27 | url: 'https://{defaultHost}', 28 | variables: { 29 | defaultHost: { 30 | default: 'www.example.com' 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | } 37 | 38 | # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. 39 | # The swagger_docs configuration option has the filename including format in 40 | # the key, this may want to be changed to avoid putting yaml in json files. 41 | # Defaults to json. Accepts ':json' and ':yaml'. 42 | config.swagger_format = :yaml 43 | end 44 | -------------------------------------------------------------------------------- /backend/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/storage/.keep -------------------------------------------------------------------------------- /backend/swagger/v1/swagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | openapi: 3.0.1 3 | info: 4 | title: API V1 5 | version: v1 6 | paths: 7 | "/api/v1/books": 8 | get: 9 | summary: Get all books 10 | tags: 11 | - Books 12 | security: 13 | - Bearer: [] 14 | parameters: 15 | - name: Authorization 16 | in: header 17 | required: true 18 | description: Authorization token 19 | schema: 20 | type: string 21 | responses: 22 | '200': 23 | description: books found 24 | '401': 25 | description: unauthorized 26 | post: 27 | summary: Create a book 28 | tags: 29 | - Books 30 | security: 31 | - Bearer: [] 32 | parameters: 33 | - name: Authorization 34 | in: header 35 | required: true 36 | description: Authorization token 37 | schema: 38 | type: string 39 | responses: 40 | '302': 41 | description: redirected 42 | '401': 43 | description: unauthorized 44 | requestBody: 45 | content: 46 | application/json: 47 | schema: 48 | type: object 49 | properties: 50 | book: 51 | title: 52 | type: string 53 | body: 54 | type: string 55 | required: 56 | - title 57 | - body 58 | application/xml: 59 | schema: 60 | type: object 61 | properties: 62 | book: 63 | title: 64 | type: string 65 | body: 66 | type: string 67 | required: 68 | - title 69 | - body 70 | "/api/v1/books/{id}": 71 | get: 72 | summary: Get a book 73 | tags: 74 | - Books 75 | security: 76 | - Bearer: [] 77 | parameters: 78 | - name: Authorization 79 | in: header 80 | required: true 81 | description: Authorization token 82 | schema: 83 | type: string 84 | - name: id 85 | in: path 86 | required: true 87 | description: ID of the book 88 | schema: 89 | type: string 90 | responses: 91 | '200': 92 | description: book found 93 | '404': 94 | description: book not found 95 | '401': 96 | description: unauthorized 97 | servers: 98 | - url: https://{defaultHost} 99 | variables: 100 | defaultHost: 101 | default: www.example.com 102 | -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /backend/test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module ApplicationCable 6 | class ConnectionTest < ActionCable::Connection::TestCase 7 | # test "connects with cookies" do 8 | # cookies.signed[:user_id] = 42 9 | # 10 | # connect 11 | # 12 | # assert_equal connection.user_id, "42" 13 | # end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /backend/test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/controllers/.keep -------------------------------------------------------------------------------- /backend/test/controllers/api/v1/books_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Api 6 | module V1 7 | class BooksControllerTest < ActionDispatch::IntegrationTest 8 | setup do 9 | @book = books(:one) 10 | create(:doorkeeper_application) 11 | access_token = create(:doorkeeper_access_token).token 12 | @token = "Bearer #{access_token}" 13 | end 14 | 15 | test 'should get index' do 16 | get(api_v1_books_url, headers: { 'Authorization' => @token }) 17 | assert_response :success 18 | end 19 | 20 | test 'should not get index if signed out' do 21 | get(api_v1_books_url, headers: { 'Authorization' => nil }) 22 | assert_response :unauthorized 23 | end 24 | 25 | test 'should get new' do 26 | get(new_api_v1_book_url, headers: { 'Authorization' => @token }) 27 | assert_response :success 28 | end 29 | 30 | test 'should create book' do 31 | assert_difference('Book.count') do 32 | post api_v1_books_url, 33 | params: { book: { body: @book.body, title: @book.title } }, 34 | headers: { 'Authorization' => @token } 35 | end 36 | assert_redirected_to api_v1_book_url(Book.last) 37 | end 38 | 39 | test 'should show book' do 40 | get api_v1_book_url(@book), headers: { 'Authorization' => @token } 41 | assert_response :success 42 | end 43 | 44 | test 'should get edit' do 45 | get edit_api_v1_book_url(@book), headers: { 'Authorization' => @token } 46 | assert_response :success 47 | end 48 | 49 | test 'should update book' do 50 | patch api_v1_book_url(@book), 51 | params: { book: { body: @book.body, title: @book.title } }, 52 | headers: { 'Authorization' => @token } 53 | assert_redirected_to api_v1_book_url(@book) 54 | end 55 | 56 | test 'should destroy book' do 57 | assert_difference('Book.count', -1) do 58 | delete api_v1_book_url(@book), 59 | headers: { 'Authorization' => @token } 60 | end 61 | 62 | assert_redirected_to api_v1_books_url 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /backend/test/controllers/books_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class BooksControllerTest < ActionDispatch::IntegrationTest 6 | setup do 7 | @book = books(:one) 8 | sign_in users(:admin) 9 | end 10 | 11 | test 'should get index' do 12 | get books_url 13 | assert_response :success 14 | end 15 | 16 | test 'should not index if signed out' do 17 | sign_out users(:admin) 18 | get books_url 19 | assert_response :redirect 20 | end 21 | 22 | test 'should get new' do 23 | get new_book_url 24 | assert_response :success 25 | end 26 | 27 | test 'should create book' do 28 | assert_difference('Book.count') do 29 | post books_url, params: { book: { body: @book.body, title: @book.title } } 30 | end 31 | 32 | assert_redirected_to book_url(Book.last) 33 | end 34 | 35 | test 'should show book' do 36 | get book_url(@book) 37 | assert_response :success 38 | end 39 | 40 | test 'should get edit' do 41 | get edit_book_url(@book) 42 | assert_response :success 43 | end 44 | 45 | test 'should update book' do 46 | patch book_url(@book), params: { book: { body: @book.body, title: @book.title } } 47 | assert_redirected_to book_url(@book) 48 | end 49 | 50 | test 'should destroy book' do 51 | assert_difference('Book.count', -1) do 52 | delete book_url(@book) 53 | end 54 | 55 | assert_redirected_to books_url 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /backend/test/controllers/pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class PagesControllerTest < ActionDispatch::IntegrationTest 6 | setup do 7 | create(:doorkeeper_application) 8 | end 9 | test 'should get home' do 10 | get root_path 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /backend/test/factories/books_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | # Create a book with a title and a body 5 | factory :book do 6 | title { Faker::Book.title } 7 | body { Faker::Lorem.paragraph } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /backend/test/factories/doorkeeper/doorkeeper_access_tokens_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :doorkeeper_access_token, class: 'Doorkeeper::AccessToken' do 5 | association :application, factory: :doorkeeper_application 6 | expires_in { 1.hour } 7 | resource_owner_id { create(:user).id } 8 | refresh_token do 9 | loop do 10 | # generate a random token string and return it 11 | # unless there is already another token with the same string 12 | token = SecureRandom.hex(32) 13 | break token unless Doorkeeper::AccessToken.exists?(refresh_token: token) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /backend/test/factories/doorkeeper/doorkeeper_applications_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :doorkeeper_application, class: 'Doorkeeper::Application' do 5 | name { Faker::App.name } 6 | redirect_uri { '' } 7 | scopes { '' } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /backend/test/factories/users_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :user do 5 | email { Faker::Internet.email } 6 | password { Faker::Internet.password(min_length: Devise.password_length.first) } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /backend/test/fixtures/books.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | title: MyString 5 | body: MyText 6 | 7 | two: 8 | title: MyString 9 | body: MyText 10 | -------------------------------------------------------------------------------- /backend/test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/fixtures/files/.keep -------------------------------------------------------------------------------- /backend/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 | user: 8 | id: 1 9 | email: "user@test.com" 10 | role: "user" 11 | admin: 12 | id: 2 13 | email: "admin@test.com" 14 | role: "admin" 15 | -------------------------------------------------------------------------------- /backend/test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/helpers/.keep -------------------------------------------------------------------------------- /backend/test/helpers/api_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require 'net/http' 5 | require 'rails/test_help' 6 | 7 | class ApiTestHelper 8 | # Post sign up request to api 9 | def api_sign_up(email, password, _client_id) 10 | uri = URI('http://localhost:3000/api/v1/users') 11 | http = Net::HTTP.new(uri.host, uri.port) 12 | request = Net::HTTP::Post.new(uri.path) 13 | request.set_form_data( 14 | { 15 | 'email' => email, 16 | 'password' => password, 17 | 'client_id' => test_client_id 18 | } 19 | ) 20 | http.request(request) 21 | end 22 | 23 | # Post sign in request to api 24 | def api_sign_in(grant_type, email, password, client_id, client_secret) 25 | uri = URI('http://localhost:3000/api/v1/oauth/token') 26 | http = Net::HTTP.new(uri.host, uri.port) 27 | request = Net::HTTP::Post.new(uri.path) 28 | request.set_form_data( 29 | { 30 | 'grant_type' => grant_type, 31 | 'email' => email, 32 | 'password' => password, 33 | 'client_id' => client_id, 34 | 'client_secret' => client_secret 35 | } 36 | ) 37 | http.request(request) 38 | end 39 | 40 | # Post sign out request to api 41 | def post_sign_out(token, client_id, client_secret) 42 | uri = URI('http://localhost:3000/api/v1/oauth/revoke') 43 | http = Net::HTTP.new(uri.host, uri.port) 44 | request = Net::HTTP::Post.new(uri.path) 45 | request.set_form_data( 46 | { 47 | 'token' => token, 48 | 'client_id' => client_id, 49 | 'client_secret' => client_secret 50 | } 51 | ) 52 | http.request(request) 53 | end 54 | 55 | # Set bearer token and access api end point 56 | def get_authorized_api_end_point(token, path) 57 | uri = URI(path) 58 | http = Net::HTTP.new(uri.host, uri.port) 59 | request = Net::HTTP::Get.new(uri.path) 60 | request.headers['Authorization'] = "Bearer #{token}" 61 | http.request(request) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /backend/test/helpers/doorkeeper_params.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Helpers 4 | module DoorkeeperParams 5 | def oauth_token_params(user, application, grant_type = Doorkeeper.config.grant_flows.first) 6 | { 7 | grant_type: grant_type, 8 | email: user.email, 9 | password: user.password, 10 | client_id: application.uid, 11 | client_secret: application.secret 12 | } 13 | end 14 | 15 | def oauth_revoke_params(token, grant_type = 'token') 16 | { 17 | grant_type: grant_type, 18 | token: token.token, 19 | client_id: token.application.uid, 20 | client_secret: token.application.secret 21 | } 22 | end 23 | 24 | def oauth_refresh_token_params(token) 25 | { 26 | grant_type: 'refresh_token', 27 | refresh_token: token.refresh_token, 28 | client_id: token.application.uid, 29 | client_secret: token.application.secret 30 | } 31 | end 32 | 33 | def oauth_register_params(user, application) 34 | { 35 | email: user.email, 36 | password: user.password, 37 | client_id: application.uid 38 | } 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /backend/test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/integration/.keep -------------------------------------------------------------------------------- /backend/test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/mailers/.keep -------------------------------------------------------------------------------- /backend/test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/models/.keep -------------------------------------------------------------------------------- /backend/test/models/book_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class BookTest < ActiveSupport::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /backend/test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class UserTest < ActiveSupport::TestCase 6 | test 'have a valid factory' do 7 | assert build(:user).valid? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /backend/test/requests/registrations_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | class RegistrationsControllerTest < ActionDispatch::IntegrationTest 7 | def setup 8 | @application = create(:doorkeeper_application) 9 | end 10 | 11 | test 'should create an user then generate access token' do 12 | user = build(:user) 13 | 14 | assert_difference(['Doorkeeper::AccessToken.count', 'User.count'], 1) do 15 | post(api_v1_user_registration_url, 16 | params: oauth_register_params(user, @application), 17 | as: :json) 18 | end 19 | 20 | assert_response :success 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /backend/test/requests/token_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Doorkeeper 6 | class TokensControllerTest < ActionDispatch::IntegrationTest 7 | def setup 8 | @user = create(:user) 9 | @application = create(:doorkeeper_application) 10 | @token = create(:doorkeeper_access_token, resource_owner_id: @user.id) 11 | end 12 | 13 | test 'should generate a new oauth access token' do 14 | assert_difference('Doorkeeper::AccessToken.count') do 15 | post(oauth_token_url, 16 | params: oauth_token_params(@user, @application), 17 | as: :json) 18 | end 19 | 20 | assert_response :success 21 | end 22 | 23 | test 'should not generate a new oauth access token with wrong user information' do 24 | invalid_user = build(:user) 25 | 26 | assert_no_difference('Doorkeeper::AccessToken.count') do 27 | post(oauth_token_url, 28 | params: oauth_token_params(invalid_user, @application), 29 | as: :json) 30 | end 31 | 32 | assert_response :bad_request 33 | end 34 | 35 | test 'should generate access token with refresh token' do 36 | assert_difference('Doorkeeper::AccessToken.count') do 37 | post(oauth_token_url, 38 | params: oauth_refresh_token_params(@token), 39 | as: :json) 40 | end 41 | 42 | assert_response :success 43 | end 44 | 45 | test 'should revoke access token' do 46 | assert_changes -> { @token.revoked_at } do 47 | post(oauth_revoke_url, 48 | params: oauth_revoke_params(@token), 49 | as: :json) 50 | 51 | @token.reload 52 | end 53 | 54 | assert_response :success 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /backend/test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/test/system/.keep -------------------------------------------------------------------------------- /backend/test/system/books_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'application_system_test_case' 4 | 5 | class BooksTest < ApplicationSystemTestCase 6 | setup do 7 | @book = books(:one) 8 | end 9 | 10 | test 'visiting the index' do 11 | visit books_url 12 | assert_selector 'h1', text: 'Books' 13 | end 14 | 15 | test 'should create book' do 16 | visit books_url 17 | click_on 'New book' 18 | 19 | fill_in 'Body', with: @book.body 20 | fill_in 'Title', with: @book.title 21 | click_on 'Create Book' 22 | 23 | assert_text 'Book was successfully created' 24 | click_on 'Back' 25 | end 26 | 27 | test 'should update Book' do 28 | visit book_url(@book) 29 | click_on 'Edit this book', match: :first 30 | 31 | fill_in 'Body', with: @book.body 32 | fill_in 'Title', with: @book.title 33 | click_on 'Update Book' 34 | 35 | assert_text 'Book was successfully updated' 36 | click_on 'Back' 37 | end 38 | 39 | test 'should destroy Book' do 40 | visit book_url(@book) 41 | click_on 'Destroy this book', match: :first 42 | 43 | assert_text 'Book was successfully destroyed' 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /backend/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 | require 'helpers/doorkeeper_params' 7 | 8 | module ActiveSupport 9 | class TestCase 10 | include FactoryBot::Syntax::Methods 11 | include Helpers::DoorkeeperParams 12 | # Run tests in parallel with specified workers 13 | parallelize(workers: :number_of_processors) 14 | 15 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 16 | fixtures :all 17 | 18 | include Devise::Test::IntegrationHelpers 19 | 20 | # Add more helper methods to be used by all tests here... 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /backend/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/tmp/.keep -------------------------------------------------------------------------------- /backend/tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/tmp/pids/.keep -------------------------------------------------------------------------------- /backend/tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/tmp/storage/.keep -------------------------------------------------------------------------------- /backend/vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/vendor/.keep -------------------------------------------------------------------------------- /backend/vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/backend/vendor/javascript/.keep -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | ], 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: "latest", 17 | sourceType: "module", 18 | }, 19 | plugins: ["react", "@typescript-eslint"], 20 | rules: { 21 | // suppress errors for missing 'import React' in files 22 | "react/react-in-jsx-scope": "off", 23 | // allow jsx syntax in js files (for next.js project) 24 | "react/jsx-filename-extension": [1, { extensions: [".ts", ".tsx"] }], //should add ".ts" if typescript project 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.9.0", 7 | "@emotion/styled": "^11.8.1", 8 | "@mui/icons-material": "^5.6.0", 9 | "@mui/material": "^5.6.0", 10 | "@reduxjs/toolkit": "^1.8.1", 11 | "@testing-library/jest-dom": "^4.2.4", 12 | "@testing-library/react": "^9.5.0", 13 | "@testing-library/user-event": "^7.2.1", 14 | "@types/jest": "^24.9.1", 15 | "@types/node": "^12.20.47", 16 | "@types/react": "^16.14.24", 17 | "@types/react-redux": "^7.1.23", 18 | "axios": "^0.26.1", 19 | "dotenv": "^16.0.0", 20 | "react": "^18.0.0", 21 | "react-dom": "^18.0.0", 22 | "react-redux": "^7.2.8", 23 | "react-router-dom": "^6.3.0", 24 | "react-scripts": "5.0.0", 25 | "typescript": "~4.1.5" 26 | }, 27 | "scripts": { 28 | "start": "PORT=3001 react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/react-dom": "^18.0.0", 50 | "@typescript-eslint/eslint-plugin": "^5.18.0", 51 | "@typescript-eslint/parser": "^5.18.0", 52 | "eslint": "^8.13.0", 53 | "eslint-plugin-react": "^7.29.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Redux App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deanout/react-wishlist-series/25ec3cfe70ce872d3d87b1999bc6cc2f93b34952/frontend/src/App.css -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './app/store'; 5 | import App from './App'; 6 | 7 | test('renders learn react link', () => { 8 | const { getByText } = render( 9 | 10 | 11 | 12 | ); 13 | 14 | expect(getByText(/learn/i)).toBeInTheDocument(); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 2 | import './App.css'; 3 | import AppBar from './app/features/appbar/AppBar'; 4 | import Dashboard from './app/features/dashboard/Dashboard'; 5 | import PrivateRoute from './app/features/routes/PrivateRoute'; 6 | import PublicOnlyRoute from './app/features/routes/PublicOnlyRoute'; 7 | import Login from './app/features/sessions/Login'; 8 | import Logout from './app/features/sessions/Logout'; 9 | import PersistLogin from './app/features/sessions/PersistLogin'; 10 | import Signup from './app/features/sessions/Signup'; 11 | import UpdateProfile from './app/features/sessions/UpdateProfile'; 12 | 13 | function App() { 14 | return ( 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 | }> 23 | 25 | 26 | 27 | } /> 28 | 30 | 31 | 32 | } /> 33 | 35 | 36 | 37 | }/> 38 | 40 | 41 | 42 | }/> 43 | 45 | 46 | 47 | }/> 48 | 49 | 50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /frontend/src/app/api/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const instance = axios.create({ 4 | baseURL: process.env.REACT_APP_BACKEND_URL, 5 | headers: { 6 | "Content-Type": "application/json", 7 | withCredentials: true, 8 | }, 9 | }); 10 | 11 | export default instance; 12 | -------------------------------------------------------------------------------- /frontend/src/app/api/sessionAPI.ts: -------------------------------------------------------------------------------- 1 | import axios from "./axios"; 2 | 3 | const LOGIN_URL = "/oauth/token"; 4 | const SIGNUP_URL = "/users"; 5 | const UPDATE_PROFILE_URL = "/users"; 6 | const LOGOUT_URL = "/oauth/revoke"; 7 | const CURRENT_USER_URL = "/users/me"; 8 | 9 | const CLIENT_ID = process.env.REACT_APP_CLIENT_ID; 10 | const CLIENT_SECRET = process.env.REACT_APP_CLIENT_SECRET; 11 | 12 | export async function createUserWithEmailAndPassword( 13 | email: string, 14 | password: string 15 | ) { 16 | const data = { 17 | email: email, 18 | password: password, 19 | client_id: CLIENT_ID, 20 | }; 21 | 22 | return axios 23 | .post(SIGNUP_URL, data) 24 | .then((response: any) => { 25 | return response.data; 26 | }) 27 | .catch((error: any) => { 28 | return error.response.data; 29 | }); 30 | } 31 | 32 | export async function loginWithEmailAndPassword( 33 | email: string, 34 | password: string 35 | ) { 36 | const data = { 37 | grant_type: "password", 38 | email: email, 39 | password: password, 40 | client_id: CLIENT_ID, 41 | client_secret: CLIENT_SECRET, 42 | }; 43 | 44 | return axios 45 | .post(LOGIN_URL, data) 46 | .then((response: any) => { 47 | return response.data; 48 | }) 49 | .catch((error: any) => { 50 | return error.response.data; 51 | }); 52 | } 53 | 54 | export async function updateUserProfile( 55 | currentPassword: string, 56 | token: string | undefined, 57 | email?: string, 58 | password?: string 59 | ) { 60 | const data = { 61 | current_password: currentPassword, 62 | email: email, 63 | password: password, 64 | client_id: CLIENT_ID, 65 | client_secret: CLIENT_SECRET, 66 | }; 67 | const config = { 68 | headers: { 69 | Authorization: `Bearer ${token}`, 70 | }, 71 | }; 72 | 73 | return axios 74 | .patch(UPDATE_PROFILE_URL, data, config) 75 | .then((response: any) => { 76 | return response.data; 77 | }) 78 | .catch((error: any) => { 79 | return error.response.data; 80 | }); 81 | } 82 | 83 | export async function logoutUserWithToken(token: string) { 84 | const data = { 85 | token: token, 86 | client_id: CLIENT_ID, 87 | client_secret: CLIENT_SECRET, 88 | }; 89 | 90 | return axios 91 | .post(LOGOUT_URL, data) 92 | .then((response: any) => { 93 | return response.data; 94 | }) 95 | .catch((error: any) => { 96 | return error.response.data; 97 | }); 98 | } 99 | 100 | export async function requestAccessTokenWithRefreshToken(refreshToken: string) { 101 | const data = { 102 | grant_type: "refresh_token", 103 | refresh_token: refreshToken, 104 | client_id: CLIENT_ID, 105 | client_secret: CLIENT_SECRET, 106 | }; 107 | 108 | return axios 109 | .post(LOGIN_URL, data) 110 | .then((response: any) => { 111 | return response.data; 112 | }) 113 | .catch((error: any) => { 114 | return error.response.data; 115 | }); 116 | } 117 | 118 | export async function getCurrentUser(accessToken: string) { 119 | const config = { 120 | headers: { 121 | Authorization: `Bearer ${accessToken}`, 122 | }, 123 | }; 124 | 125 | return axios 126 | .get(CURRENT_USER_URL, config) 127 | .then((response: any) => { 128 | return response.data; 129 | }) 130 | .catch((error: any) => { 131 | return error.response.data; 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /frontend/src/app/features/appbar/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Box from '@mui/material/Box'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Typography from '@mui/material/Typography'; 7 | import Menu from '@mui/material/Menu'; 8 | import MenuIcon from '@mui/icons-material/Menu'; 9 | import Container from '@mui/material/Container'; 10 | import Avatar from '@mui/material/Avatar'; 11 | import Button from '@mui/material/Button'; 12 | import Tooltip from '@mui/material/Tooltip'; 13 | import MenuItem from '@mui/material/MenuItem'; 14 | import { To, useNavigate } from 'react-router-dom'; 15 | import { useSelector } from 'react-redux'; 16 | import { RootState } from '../../store'; 17 | 18 | const pages = ['Products', 'Pricing', 'Blog']; 19 | const settings = ['Profile', 'Account', 'Dashboard', 'Logout']; 20 | 21 | const ResponsiveAppBar = () => { 22 | const [anchorElNav, setAnchorElNav] = React.useState(null); 23 | const [anchorElUser, setAnchorElUser] = React.useState(null); 24 | const navigate = useNavigate(); 25 | const accessToken = useSelector((state : RootState) => state.session.accessToken); 26 | const loading = useSelector((state : RootState) => state.session.loading); 27 | const currentUser = useSelector((state : RootState) => state.session.currentUser); 28 | 29 | 30 | const handleOpenNavMenu = (event: React.MouseEvent) => { 31 | setAnchorElNav(event.currentTarget); 32 | }; 33 | const handleOpenUserMenu = (event: React.MouseEvent) => { 34 | setAnchorElUser(event.currentTarget); 35 | }; 36 | 37 | const handleCloseNavMenu = () => { 38 | setAnchorElNav(null); 39 | }; 40 | 41 | const handleCloseUserMenu = () => { 42 | setAnchorElUser(null); 43 | }; 44 | 45 | function handleNavigate(route: To, event: React.MouseEvent) { 46 | event?.preventDefault(); 47 | navigate(route); 48 | } 49 | 50 | function handleLogout(event: React.MouseEvent) { 51 | event?.preventDefault(); 52 | handleCloseUserMenu(); 53 | navigate('/logout'); 54 | } 55 | 56 | function handleUpdateProfileAccount(event: React.MouseEvent) { 57 | event?.preventDefault(); 58 | handleCloseUserMenu(); 59 | navigate('/update-profile'); 60 | } 61 | 62 | let sessionLinks; 63 | if (accessToken) { 64 | sessionLinks = 65 | 66 | {currentUser?.email} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 89 | handleUpdateProfileAccount(event)}> 90 | Update Profile 91 | 92 | handleLogout(event)}> 93 | Logout 94 | 95 | 96 | ; 97 | } else if (!accessToken && !loading) { 98 | sessionLinks = <> 99 | 105 | 111 | 112 | } 113 | 114 | 115 | return ( 116 | 117 | 118 | 119 | 125 | WishList 126 | 127 | 128 | 129 | 137 | 138 | 139 | 157 | {pages.map((page) => ( 158 | 159 | {page} 160 | 161 | ))} 162 | 163 | 164 | 170 | Wishlist 171 | 172 | 173 | 179 | 180 | {sessionLinks} 181 | 182 | 183 | 184 | ); 185 | }; 186 | export default ResponsiveAppBar; 187 | -------------------------------------------------------------------------------- /frontend/src/app/features/counter/Counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: 'Courier New', Courier, monospace; 22 | } 23 | 24 | .button { 25 | appearance: none; 26 | background: none; 27 | font-size: 32px; 28 | padding-left: 12px; 29 | padding-right: 12px; 30 | outline: none; 31 | border: 2px solid transparent; 32 | color: rgb(112, 76, 182); 33 | padding-bottom: 4px; 34 | cursor: pointer; 35 | background-color: rgba(112, 76, 182, 0.1); 36 | border-radius: 2px; 37 | transition: all 0.15s; 38 | } 39 | 40 | .textbox { 41 | font-size: 32px; 42 | padding: 2px; 43 | width: 64px; 44 | text-align: center; 45 | margin-right: 4px; 46 | } 47 | 48 | .button:hover, 49 | .button:focus { 50 | border: 2px solid rgba(112, 76, 182, 0.4); 51 | } 52 | 53 | .button:active { 54 | background-color: rgba(112, 76, 182, 0.2); 55 | } 56 | 57 | .asyncButton { 58 | composes: button; 59 | position: relative; 60 | } 61 | 62 | .asyncButton:after { 63 | content: ''; 64 | background-color: rgba(112, 76, 182, 0.15); 65 | display: block; 66 | position: absolute; 67 | width: 100%; 68 | height: 100%; 69 | left: 0; 70 | top: 0; 71 | opacity: 0; 72 | transition: width 1s linear, opacity 0.5s ease 1s; 73 | } 74 | 75 | .asyncButton:active:after { 76 | width: 0%; 77 | opacity: 1; 78 | transition: 0s; 79 | } 80 | -------------------------------------------------------------------------------- /frontend/src/app/features/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { useAppSelector, useAppDispatch } from '../../hooks'; 4 | import { 5 | decrement, 6 | increment, 7 | incrementByAmount, 8 | incrementAsync, 9 | incrementIfOdd, 10 | selectCount, 11 | } from './counterSlice'; 12 | import styles from './Counter.module.css'; 13 | 14 | export function Counter() { 15 | const count = useAppSelector(selectCount); 16 | const dispatch = useAppDispatch(); 17 | const [incrementAmount, setIncrementAmount] = useState('2'); 18 | 19 | const incrementValue = Number(incrementAmount) || 0; 20 | 21 | return ( 22 |
23 |
24 | 31 | {count} 32 | 39 |
40 |
41 | setIncrementAmount(e.target.value)} 46 | /> 47 | 53 | 59 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/app/features/counter/counterAPI.ts: -------------------------------------------------------------------------------- 1 | // A mock function to mimic making an async request for data 2 | export function fetchCount(amount = 1) { 3 | return new Promise<{ data: number }>((resolve) => 4 | setTimeout(() => resolve({ data: amount }), 500) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app/features/counter/counterSlice.spec.ts: -------------------------------------------------------------------------------- 1 | import counterReducer, { 2 | CounterState, 3 | increment, 4 | decrement, 5 | incrementByAmount, 6 | } from './counterSlice'; 7 | 8 | describe('counter reducer', () => { 9 | const initialState: CounterState = { 10 | value: 3, 11 | status: 'idle', 12 | }; 13 | it('should handle initial state', () => { 14 | expect(counterReducer(undefined, { type: 'unknown' })).toEqual({ 15 | value: 0, 16 | status: 'idle', 17 | }); 18 | }); 19 | 20 | it('should handle increment', () => { 21 | const actual = counterReducer(initialState, increment()); 22 | expect(actual.value).toEqual(4); 23 | }); 24 | 25 | it('should handle decrement', () => { 26 | const actual = counterReducer(initialState, decrement()); 27 | expect(actual.value).toEqual(2); 28 | }); 29 | 30 | it('should handle incrementByAmount', () => { 31 | const actual = counterReducer(initialState, incrementByAmount(2)); 32 | expect(actual.value).toEqual(5); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /frontend/src/app/features/counter/counterSlice.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { RootState, AppThunk } from "../../store"; 3 | import { fetchCount } from "./counterAPI"; 4 | 5 | export interface CounterState { 6 | value: number; 7 | status: "idle" | "loading" | "failed"; 8 | } 9 | 10 | const initialState: CounterState = { 11 | value: 0, 12 | status: "idle", 13 | }; 14 | 15 | // The function below is called a thunk and allows us to perform async logic. It 16 | // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This 17 | // will call the thunk with the `dispatch` function as the first argument. Async 18 | // code can then be executed and other actions can be dispatched. Thunks are 19 | // typically used to make async requests. 20 | export const incrementAsync = createAsyncThunk( 21 | "counter/fetchCount", 22 | async (amount: number) => { 23 | const response = await fetchCount(amount); 24 | // The value we return becomes the `fulfilled` action payload 25 | return response.data; 26 | } 27 | ); 28 | 29 | export const counterSlice = createSlice({ 30 | name: "counter", 31 | initialState, 32 | // The `reducers` field lets us define reducers and generate associated actions 33 | reducers: { 34 | increment: (state) => { 35 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 36 | // doesn't actually mutate the state because it uses the Immer library, 37 | // which detects changes to a "draft state" and produces a brand new 38 | // immutable state based off those changes 39 | state.value += 1; 40 | }, 41 | decrement: (state) => { 42 | state.value -= 1; 43 | }, 44 | // Use the PayloadAction type to declare the contents of `action.payload` 45 | incrementByAmount: (state, action: PayloadAction) => { 46 | state.value += action.payload; 47 | }, 48 | }, 49 | // The `extraReducers` field lets the slice handle actions defined elsewhere, 50 | // including actions generated by createAsyncThunk or in other slices. 51 | extraReducers: (builder) => { 52 | builder 53 | .addCase(incrementAsync.pending, (state) => { 54 | state.status = "loading"; 55 | }) 56 | .addCase(incrementAsync.fulfilled, (state, action) => { 57 | state.status = "idle"; 58 | state.value += action.payload; 59 | }); 60 | }, 61 | }); 62 | 63 | export const { increment, decrement, incrementByAmount } = counterSlice.actions; 64 | 65 | // The function below is called a selector and allows us to select a value from 66 | // the state. Selectors can also be defined inline where they're used instead of 67 | // in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` 68 | export const selectCount = (state: RootState) => state.counter.value; 69 | 70 | // We can also write thunks by hand, which may contain both sync and async logic. 71 | // Here's an example of conditionally dispatching actions based on current state. 72 | export const incrementIfOdd = 73 | (amount: number): AppThunk => 74 | (dispatch, getState) => { 75 | const currentValue = selectCount(getState()); 76 | if (currentValue % 2 === 1) { 77 | dispatch(incrementByAmount(amount)); 78 | } 79 | }; 80 | 81 | export default counterSlice.reducer; 82 | -------------------------------------------------------------------------------- /frontend/src/app/features/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux" 2 | import { RootState } from "../../store" 3 | 4 | 5 | function Dashboard() { 6 | const currentUser = useSelector((state: RootState) => state.session.currentUser) 7 | const accessToken = useSelector((state: RootState) => state.session.accessToken) 8 | const refreshToken = useSelector((state: RootState) => state.session.refreshToken) 9 | return ( 10 |
11 |

Dashboard

12 |
    13 |
  • Current User 14 |
      15 |
    • Id: {currentUser?.id}
    • 16 |
    • Email: {currentUser?.email}
    • 17 |
    • Role: {currentUser?.role}
    • 18 |
    • Created At: {currentUser?.createdAt}
    • 19 |
    20 |
  • 21 |
  • Access Token: {accessToken}
  • 22 |
  • Refresh Token: {refreshToken}
  • 23 |
24 |
25 | ) 26 | } 27 | 28 | export default Dashboard -------------------------------------------------------------------------------- /frontend/src/app/features/routes/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux'; 3 | import { Navigate, useLocation } from 'react-router-dom'; 4 | import { RootState } from '../../store'; 5 | 6 | function PrivateRoute({ children } : any ) { 7 | const loading = useSelector((state: RootState) => state.session.loading); 8 | const accessToken = useSelector((state : RootState) => state.session.accessToken); 9 | const location = useLocation(); 10 | const fromLocation = (location.state as any)?.from; 11 | const previousLocation = fromLocation ? fromLocation : { pathname: '/login'}; 12 | 13 | if (accessToken) { 14 | return children; 15 | } else if (loading) { 16 | return

Loading...

; 17 | } else if (!accessToken && !loading) { 18 | return ; 19 | } else { 20 | return

Something went wrong

; 21 | } 22 | } 23 | 24 | export default PrivateRoute -------------------------------------------------------------------------------- /frontend/src/app/features/routes/PublicOnlyRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux'; 3 | import { Navigate, useLocation } from 'react-router-dom'; 4 | import { RootState } from '../../store'; 5 | 6 | function PublicOnlyRoute({ children } : any) { 7 | const accessToken = useSelector((state : RootState) => state.session.accessToken); 8 | const loading = useSelector((state : RootState) => state.session.loading); 9 | const location = useLocation(); 10 | const fromLocation = (location.state as any)?.from; 11 | const previousLocation = fromLocation ? fromLocation : { pathname: '/'}; 12 | 13 | if (!accessToken && !loading) { 14 | return children; 15 | } else if (loading) { 16 | return

Loading...

; 17 | } else if (accessToken && !loading) { 18 | return ; 19 | } else { 20 | return

Something went wrong

; 21 | } 22 | } 23 | 24 | export default PublicOnlyRoute -------------------------------------------------------------------------------- /frontend/src/app/features/sessions/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Visibility, VisibilityOff } from "@mui/icons-material"; 2 | import { Alert, Box, Button, Card, CardActions, CardContent, Container, Divider, FormControl, FormGroup, IconButton, Input, InputAdornment, InputLabel, OutlinedInput, Typography } from "@mui/material"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { Link, useNavigate } from "react-router-dom"; 6 | import { RootState } from "../../store"; 7 | import { loginUser, resetErrorState } from "./sessionSlice"; 8 | 9 | 10 | function Login() { 11 | const emailRef = useRef(); 12 | const passwordRef = useRef(); 13 | const errorMessages = useSelector((state: RootState) => state.session.errorMessages); 14 | const [errors, setErrors] = useState>([]) 15 | const [showPassword, setShowPassword] = useState(false); 16 | const loading = false; 17 | const navigate = useNavigate(); 18 | const dispatch = useDispatch(); 19 | 20 | useEffect(() => { 21 | emailRef?.current?.focus(); 22 | if (errorMessages.length > 0) { 23 | setErrors(errorMessages); 24 | dispatch(resetErrorState()); 25 | } 26 | }, []) 27 | 28 | async function handleSubmit(event: React.FormEvent) { 29 | event.preventDefault(); 30 | setErrors([]); 31 | if (emailRef?.current === undefined 32 | || emailRef.current.value === "" 33 | || passwordRef?.current === undefined 34 | || passwordRef.current.value === "") { 35 | return setErrors(["Please fill out all fields"]) 36 | } 37 | const payload = { 38 | email: emailRef.current.value, 39 | password: passwordRef.current.value 40 | } 41 | const response = await dispatch(loginUser(payload)) as any; 42 | console.log(response); 43 | if (errorMessages.length === 0) { 44 | navigate("/"); 45 | } else { 46 | return setErrors(errorMessages); 47 | } 48 | } 49 | 50 | const passwordInput = 52 | setShowPassword(!showPassword)} 55 | onMouseDown={() => setShowPassword(!showPassword)} 56 | edge="end"> 57 | {showPassword ? : } 58 | 59 | 60 | } />; 61 | 62 | return ( 63 |
64 | 65 | 66 | 67 | 68 | 69 | Login 70 | 71 | {errors.length > 0 ? 72 | 73 | {errors.map((error, index) => { 74 | return

75 | {error} 76 |

77 | })} 78 |
79 | : <>} 80 |
81 | 82 | 83 | Email Address 84 | 85 | 86 | 87 | 88 | 89 | Password 90 | {passwordInput} 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 |
104 |
105 |
106 | 107 | 108 | 109 | 110 | Forgot Password? 111 | 112 | Create an Account! 113 | 114 | 115 |
116 |
117 | 118 |
119 | ) 120 | } 121 | 122 | export default Login -------------------------------------------------------------------------------- /frontend/src/app/features/sessions/Logout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { RootState } from '../../store'; 5 | import { logoutUser } from './sessionSlice'; 6 | 7 | function Logout() { 8 | const navigate = useNavigate(); 9 | const dispatch = useDispatch(); 10 | const refreshToken = useSelector((state : RootState) => state.session.accessToken); 11 | 12 | useEffect(() => { 13 | if (refreshToken){ 14 | dispatch(logoutUser(refreshToken)); 15 | } 16 | navigate('/login'); 17 | }, []); 18 | 19 | return ( 20 |
Logout
21 | ) 22 | } 23 | 24 | export default Logout -------------------------------------------------------------------------------- /frontend/src/app/features/sessions/PersistLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Outlet } from 'react-router-dom'; 4 | import { RootState } from '../../store'; 5 | import { refreshAccessToken } from './sessionSlice'; 6 | 7 | function PersistLogin() { 8 | const loading = useSelector((state: RootState) => state.session.loading); 9 | const accessToken = useSelector((state : RootState) => state.session.accessToken); 10 | const refreshToken = useSelector((state : RootState) => state.session.refreshToken); 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | function verifyRefreshToken() { 15 | try { 16 | dispatch(refreshAccessToken(refreshToken)); 17 | } catch (error) { 18 | console.log(error); 19 | } 20 | } 21 | if (!accessToken) { 22 | verifyRefreshToken(); 23 | } 24 | }, [accessToken, refreshToken]); 25 | 26 | return ( 27 | <> 28 | {loading ?

Loading...

: } 29 | 30 | ) 31 | } 32 | 33 | export default PersistLogin -------------------------------------------------------------------------------- /frontend/src/app/features/sessions/Signup.tsx: -------------------------------------------------------------------------------- 1 | import { Visibility, VisibilityOff } from "@mui/icons-material"; 2 | import { Alert, Box, Button, Card, CardActions, CardContent, Container, Divider, FormControl, FormGroup, FormHelperText, IconButton, Input, InputAdornment, InputLabel, OutlinedInput, Typography } from "@mui/material"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { Link, useLocation, useNavigate } from "react-router-dom"; 6 | import { RootState } from "../../store"; 7 | import { resetErrorState, signUpUser } from "./sessionSlice"; 8 | 9 | 10 | function Signup() { 11 | const emailRef = useRef(); 12 | const passwordRef = useRef(); 13 | const passwordConfirmationRef = useRef(); 14 | const errorMessages = useSelector((state: RootState) => state.session.errorMessages); 15 | 16 | const [errors, setErrors] = useState>([]) 17 | const [showPassword, setShowPassword] = useState(false); 18 | const loading = false; 19 | const navigate = useNavigate(); 20 | const dispatch = useDispatch(); 21 | 22 | useEffect(() => { 23 | emailRef?.current?.focus(); 24 | if (errorMessages !== undefined) { 25 | setErrors(errorMessages); 26 | dispatch(resetErrorState()); 27 | } 28 | }, []) 29 | 30 | async function handleSubmit(event: React.FormEvent) { 31 | event.preventDefault(); 32 | setErrors([]); 33 | if (emailRef?.current === undefined 34 | || emailRef.current.value === "" 35 | || passwordRef?.current === undefined 36 | || passwordRef.current.value === "" 37 | || passwordConfirmationRef?.current === undefined 38 | || passwordConfirmationRef.current.value === "") { 39 | return setErrors(["Please fill out all fields"]) 40 | } 41 | if (passwordRef.current.value !== passwordConfirmationRef.current.value) { 42 | return setErrors(["Passwords do not match"]) 43 | } 44 | const payload = { 45 | email: emailRef.current.value, 46 | password: passwordRef.current.value 47 | } 48 | const response = await dispatch(signUpUser(payload)) as any; 49 | 50 | console.log(response); 51 | if (errorMessages.length > 0) { 52 | navigate("/"); 53 | } else { 54 | return setErrors(errorMessages); 55 | } 56 | } 57 | 58 | const passwordInput = 60 | setShowPassword(!showPassword)} 63 | onMouseDown={() => setShowPassword(!showPassword)} 64 | edge="end"> 65 | {showPassword ? : } 66 | 67 | 68 | } />; 69 | 70 | const passwordConfirmationInput = 72 | setShowPassword(!showPassword)} 75 | onMouseDown={() => setShowPassword(!showPassword)} 76 | edge="end"> 77 | {showPassword ? : } 78 | 79 | 80 | } />; 81 | 82 | return ( 83 |
84 | 85 | 86 | 87 | 88 | 89 | Sign Up 90 | 91 | {errors.length > 0 ? 92 | 93 | {errors.map((error, index) => { 94 | return

95 | {error} 96 |

97 | })} 98 |
99 | : <>} 100 |
101 | 102 | 103 | Email Address 104 | 105 | We'll never share your email. 106 | 107 | 108 | 109 | 110 | Password 111 | {passwordInput} 112 | 113 | 114 | 115 | 116 | Password Confirmation 117 | {passwordConfirmationInput} 118 | 119 | 120 | 121 | 122 | 128 | 129 | 130 |
131 |
132 |
133 | 134 | 135 | 136 | Already have an account? Login! 137 | 138 | 139 |
140 |
141 | 142 |
143 | ) 144 | } 145 | 146 | export default Signup -------------------------------------------------------------------------------- /frontend/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './store'; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /frontend/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; 2 | import counterReducer from "./features/counter/counterSlice"; 3 | import sessionReducer from "./features/sessions/sessionSlice"; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | counter: counterReducer, 8 | session: sessionReducer, 9 | }, 10 | }); 11 | 12 | export type AppDispatch = typeof store.dispatch; 13 | export type RootState = ReturnType; 14 | export type AppThunk = ThunkAction< 15 | ReturnType, 16 | RootState, 17 | unknown, 18 | Action 19 | >; 20 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { store } from './app/store'; 6 | import { Provider } from 'react-redux'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | const rootElement = document.getElementById('root'); 10 | const root = ReactDom.createRoot(rootElement as HTMLElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | , 18 | ); 19 | 20 | // If you want your app to work offline and load faster, you can change 21 | // unregister() to register() below. Note this comes with some pitfalls. 22 | // Learn more about service workers: https://bit.ly/CRA-PWA 23 | serviceWorker.unregister(); 24 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 32 | if (publicUrl.origin !== window.location.origin) { 33 | // Our service worker won't work if PUBLIC_URL is on a different origin 34 | // from what our page is served on. This might happen if a CDN is used to 35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 36 | return; 37 | } 38 | 39 | window.addEventListener('load', () => { 40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 41 | 42 | if (isLocalhost) { 43 | // This is running on localhost. Let's check if a service worker still exists or not. 44 | checkValidServiceWorker(swUrl, config); 45 | 46 | // Add some additional logging to localhost, pointing developers to the 47 | // service worker/PWA documentation. 48 | navigator.serviceWorker.ready.then(() => { 49 | console.log( 50 | 'This web app is being served cache-first by a service ' + 51 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 52 | ); 53 | }); 54 | } else { 55 | // Is not localhost. Just register service worker 56 | registerValidSW(swUrl, config); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | function registerValidSW(swUrl: string, config?: Config) { 63 | navigator.serviceWorker 64 | .register(swUrl) 65 | .then((registration) => { 66 | registration.onupdatefound = () => { 67 | const installingWorker = registration.installing; 68 | if (installingWorker == null) { 69 | return; 70 | } 71 | installingWorker.onstatechange = () => { 72 | if (installingWorker.state === 'installed') { 73 | if (navigator.serviceWorker.controller) { 74 | // At this point, the updated precached content has been fetched, 75 | // but the previous service worker will still serve the older 76 | // content until all client tabs are closed. 77 | console.log( 78 | 'New content is available and will be used when all ' + 79 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 80 | ); 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration); 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It's the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log('Content is cached for offline use.'); 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration); 95 | } 96 | } 97 | } 98 | }; 99 | }; 100 | }) 101 | .catch((error) => { 102 | console.error('Error during service worker registration:', error); 103 | }); 104 | } 105 | 106 | function checkValidServiceWorker(swUrl: string, config?: Config) { 107 | // Check if the service worker can be found. If it can't reload the page. 108 | fetch(swUrl, { 109 | headers: { 'Service-Worker': 'script' }, 110 | }) 111 | .then((response) => { 112 | // Ensure service worker exists, and that we really are getting a JS file. 113 | const contentType = response.headers.get('content-type'); 114 | if ( 115 | response.status === 404 || 116 | (contentType != null && contentType.indexOf('javascript') === -1) 117 | ) { 118 | // No service worker found. Probably a different app. Reload the page. 119 | navigator.serviceWorker.ready.then((registration) => { 120 | registration.unregister().then(() => { 121 | window.location.reload(); 122 | }); 123 | }); 124 | } else { 125 | // Service worker found. Proceed as normal. 126 | registerValidSW(swUrl, config); 127 | } 128 | }) 129 | .catch(() => { 130 | console.log( 131 | 'No internet connection found. App is running in offline mode.' 132 | ); 133 | }); 134 | } 135 | 136 | export function unregister() { 137 | if ('serviceWorker' in navigator) { 138 | navigator.serviceWorker.ready 139 | .then((registration) => { 140 | registration.unregister(); 141 | }) 142 | .catch((error) => { 143 | console.error(error.message); 144 | }); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------