├── backend
├── log
│ └── .keep
├── tmp
│ ├── .keep
│ ├── pids
│ │ └── .keep
│ └── storage
│ │ └── .keep
├── lib
│ ├── tasks
│ │ └── .keep
│ └── assets
│ │ └── .keep
├── storage
│ └── .keep
├── vendor
│ ├── .keep
│ └── javascript
│ │ └── .keep
├── public
│ ├── favicon.ico
│ ├── apple-touch-icon.png
│ ├── apple-touch-icon-precomposed.png
│ ├── robots.txt
│ ├── 500.html
│ ├── 422.html
│ └── 404.html
├── test
│ ├── helpers
│ │ ├── .keep
│ │ ├── doorkeeper_params.rb
│ │ └── api_helpers.rb
│ ├── mailers
│ │ └── .keep
│ ├── models
│ │ ├── .keep
│ │ ├── book_test.rb
│ │ └── user_test.rb
│ ├── system
│ │ ├── .keep
│ │ └── books_test.rb
│ ├── controllers
│ │ ├── .keep
│ │ ├── pages_controller_test.rb
│ │ ├── books_controller_test.rb
│ │ └── api
│ │ │ └── v1
│ │ │ └── books_controller_test.rb
│ ├── fixtures
│ │ ├── files
│ │ │ └── .keep
│ │ ├── books.yml
│ │ └── users.yml
│ ├── integration
│ │ └── .keep
│ ├── application_system_test_case.rb
│ ├── factories
│ │ ├── users_factory.rb
│ │ ├── books_factory.rb
│ │ └── doorkeeper
│ │ │ ├── doorkeeper_applications_factory.rb
│ │ │ └── doorkeeper_access_tokens_factory.rb
│ ├── channels
│ │ └── application_cable
│ │ │ └── connection_test.rb
│ ├── requests
│ │ ├── registrations_controller_test.rb
│ │ └── token_controller_test.rb
│ └── test_helper.rb
├── app
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── config
│ │ │ └── manifest.js
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── models
│ │ ├── concerns
│ │ │ └── .keep
│ │ ├── book.rb
│ │ ├── application_record.rb
│ │ └── user.rb
│ ├── controllers
│ │ ├── concerns
│ │ │ ├── .keep
│ │ │ └── doorkeeper_registerable.rb
│ │ ├── application_controller.rb
│ │ ├── pages_controller.rb
│ │ ├── doorkeeper
│ │ │ ├── application_metal_controller.rb
│ │ │ └── tokens_controller.rb
│ │ ├── swagger
│ │ │ ├── responses
│ │ │ │ ├── error_response.rb
│ │ │ │ └── users
│ │ │ │ │ ├── token_responses.rb
│ │ │ │ │ └── registration_responses.rb
│ │ │ ├── inputs
│ │ │ │ └── users
│ │ │ │ │ ├── registration_input.rb
│ │ │ │ │ └── token_inputs.rb
│ │ │ └── controllers
│ │ │ │ └── users
│ │ │ │ ├── registrations_controller.rb
│ │ │ │ └── tokens_controller.rb
│ │ ├── api_controller.rb
│ │ ├── api
│ │ │ └── v1
│ │ │ │ ├── users_controller.rb
│ │ │ │ ├── android
│ │ │ │ └── books_controller.rb
│ │ │ │ ├── users
│ │ │ │ └── registrations_controller.rb
│ │ │ │ └── books_controller.rb
│ │ ├── apidocs_controller.rb
│ │ └── books_controller.rb
│ ├── views
│ │ ├── layouts
│ │ │ ├── mailer.text.erb
│ │ │ ├── mailer.html.erb
│ │ │ ├── application.html.erb
│ │ │ └── swagger.html.erb
│ │ ├── books
│ │ │ ├── show.json.jbuilder
│ │ │ ├── index.json.jbuilder
│ │ │ ├── _book.json.jbuilder
│ │ │ ├── new.html.erb
│ │ │ ├── edit.html.erb
│ │ │ ├── _book.html.erb
│ │ │ ├── show.html.erb
│ │ │ ├── index.html.erb
│ │ │ └── _form.html.erb
│ │ └── pages
│ │ │ └── home.html.erb
│ ├── helpers
│ │ ├── books_helper.rb
│ │ ├── pages_helper.rb
│ │ └── application_helper.rb
│ ├── channels
│ │ └── application_cable
│ │ │ ├── channel.rb
│ │ │ └── connection.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ ├── javascript
│ │ ├── application.js
│ │ └── controllers
│ │ │ ├── hello_controller.js
│ │ │ ├── application.js
│ │ │ └── index.js
│ └── jobs
│ │ └── application_job.rb
├── db
│ ├── seeds
│ │ ├── production.rb
│ │ ├── test.rb
│ │ └── development.rb
│ ├── migrate
│ │ ├── 20220404025750_add_role_to_user.rb
│ │ ├── 20220404025649_create_books.rb
│ │ ├── 20220404025721_devise_create_users.rb
│ │ └── 20220404030809_create_doorkeeper_tables.rb
│ ├── seeds.rb
│ └── schema.rb
├── .ruby-version
├── .rspec
├── scripts
│ ├── swagger.sh
│ └── rubocop.sh
├── .rubocop.yml
├── bin
│ ├── importmap
│ ├── rake
│ ├── rails
│ ├── setup
│ └── bundle
├── spec
│ ├── support
│ │ └── factory_bot.rb
│ ├── swagger_helper.rb
│ ├── integration
│ │ └── books_spec.rb
│ ├── rails_helper.rb
│ └── spec_helper.rb
├── config
│ ├── environment.rb
│ ├── boot.rb
│ ├── cable.yml
│ ├── initializers
│ │ ├── cors.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── permissions_policy.rb
│ │ ├── assets.rb
│ │ ├── inflections.rb
│ │ ├── rswag-ui.rb
│ │ ├── rswag_api.rb
│ │ └── content_security_policy.rb
│ ├── importmap.rb
│ ├── credentials.yml.enc
│ ├── routes.rb
│ ├── routes
│ │ └── api.rb
│ ├── application.rb
│ ├── locales
│ │ ├── en.yml
│ │ ├── devise.en.yml
│ │ └── doorkeeper.en.yml
│ ├── storage.yml
│ ├── puma.rb
│ ├── environments
│ │ ├── test.rb
│ │ ├── development.rb
│ │ └── production.rb
│ └── database.yml
├── config.ru
├── Rakefile
├── .gitignore
├── README.md
├── swagger
│ └── v1
│ │ └── swagger.yaml
├── .rubocop_todo.yml
└── Gemfile
├── frontend
├── src
│ ├── App.css
│ ├── react-app-env.d.ts
│ ├── app
│ │ ├── features
│ │ │ ├── counter
│ │ │ │ ├── counterAPI.ts
│ │ │ │ ├── counterSlice.spec.ts
│ │ │ │ ├── Counter.module.css
│ │ │ │ ├── Counter.tsx
│ │ │ │ └── counterSlice.ts
│ │ │ ├── sessions
│ │ │ │ ├── Logout.tsx
│ │ │ │ ├── PersistLogin.tsx
│ │ │ │ ├── Login.tsx
│ │ │ │ └── Signup.tsx
│ │ │ ├── routes
│ │ │ │ ├── PrivateRoute.tsx
│ │ │ │ └── PublicOnlyRoute.tsx
│ │ │ ├── dashboard
│ │ │ │ └── Dashboard.tsx
│ │ │ └── appbar
│ │ │ │ └── AppBar.tsx
│ │ ├── api
│ │ │ ├── axios.ts
│ │ │ └── sessionAPI.ts
│ │ ├── hooks.ts
│ │ └── store.ts
│ ├── setupTests.ts
│ ├── index.css
│ ├── App.test.tsx
│ ├── index.tsx
│ ├── logo.svg
│ ├── App.tsx
│ └── serviceWorker.ts
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── tsconfig.json
├── .eslintrc.js
├── package.json
└── README.md
├── README.md
└── .github
└── workflows
└── rubyonrails.yml
/backend/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/system/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/tmp/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/db/seeds/production.rb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/vendor/javascript/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.0.3
2 |
--------------------------------------------------------------------------------
/backend/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/backend/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/scripts/swagger.sh:
--------------------------------------------------------------------------------
1 | rake rswag:specs:swaggerize
--------------------------------------------------------------------------------
/backend/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from: .rubocop_todo.yml
2 |
--------------------------------------------------------------------------------
/backend/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/backend/scripts/rubocop.sh:
--------------------------------------------------------------------------------
1 | bundle exec rubocop --parallel -A
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/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/models/book.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Book < ApplicationRecord
4 | end
5 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deanout/react-wishlist-series/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deanout/react-wishlist-series/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deanout/react-wishlist-series/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/backend/app/views/books/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.partial! 'books/book', book: @book
4 |
--------------------------------------------------------------------------------
/backend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/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/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | end
5 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
7 | <% book.errors.each do |error| %>
8 | <%= error.full_message %>
9 | <% end %>
10 |
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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/.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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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/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 |
18 | <%= link_to "Web Books", books_path %>
19 | <%= link_to "API V1 Books", api_v1_books_path %>
20 | <%= link_to "API V1 Android Books", api_v1_android_books_path %>
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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/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/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React Redux App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 | dispatch(decrement())}
28 | >
29 | -
30 |
31 | {count}
32 | dispatch(increment())}
36 | >
37 | +
38 |
39 |
40 |
41 | setIncrementAmount(e.target.value)}
46 | />
47 | dispatch(incrementByAmount(incrementValue))}
50 | >
51 | Add Amount
52 |
53 | dispatch(incrementAsync(incrementValue))}
56 | >
57 | Add Async
58 |
59 | dispatch(incrementIfOdd(incrementValue))}
62 | >
63 | Add If Odd
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/.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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
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/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/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 |
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/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 |
96 | ;
97 | } else if (!accessToken && !loading) {
98 | sessionLinks = <>
99 | handleNavigate("/signup", event)}
101 | sx={{ my: 2, color: 'white', display: 'block' }}
102 | >
103 | Create Account
104 |
105 | handleNavigate("/login", event)}
107 | sx={{ my: 2, color: 'white', display: 'block' }}
108 | >
109 | Login
110 |
111 | >
112 | }
113 |
114 |
115 | return (
116 |
117 |
118 |
119 |
125 | WishList
126 |
127 |
128 |
129 |
137 |
138 |
139 |
163 |
164 |
170 | Wishlist
171 |
172 |
173 | handleNavigate("/", event)}
175 | sx={{ my: 2, color: 'white', display: 'block' }}
176 | >
177 | Home
178 |
179 |
180 | {sessionLinks}
181 |
182 |
183 |
184 | );
185 | };
186 | export default ResponsiveAppBar;
187 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------