diff --git a/Gemfile b/Gemfile index 9ddd80d..754ea10 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ gem 'thor', '0.19.1' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 1.2' gem 'devise' +gem 'doorkeeper', '2.0' group :doc do # bundle exec rake doc:rails generates the API under doc/api. gem 'sdoc', require: false @@ -46,4 +47,5 @@ gem 'remotipart', '~> 1.2' gem 'friendly_numbers' gem 'cancancan', '~> 1.17' gem 'rack-cors' -gem 'active_model_serializers', '~> 0.10.0' \ No newline at end of file +gem 'active_model_serializers', '~> 0.10.0' +gem 'foreigner' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 71ab5f3..2b0357c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,7 +50,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) devise (3.5.10) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -58,6 +58,8 @@ GEM responders thread_safe (~> 0.1) warden (~> 1.2.3) + doorkeeper (2.0.0) + railties (>= 3.1) erubis (2.7.0) execjs (2.8.1) ffi (1.15.5) @@ -65,6 +67,8 @@ GEM railties (>= 3.2, < 8.0) font-awesome-sass (6.2.1) sassc (~> 2.0) + foreigner (1.7.4) + activerecord (>= 3.0.0) friendly_numbers (0.6.0) hike (1.2.3) i18n (0.9.5) @@ -84,7 +88,7 @@ GEM method_source (0.8.2) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mini_mime (1.1.2) minitest (4.7.5) multi_json (1.15.0) @@ -95,7 +99,7 @@ GEM cocaine (~> 0.5.3) mime-types parallel (1.19.2) - parser (3.2.0.0) + parser (3.2.1.0) ast (~> 2.4.1) popper_js (1.12.9) pry (0.10.4) @@ -143,7 +147,7 @@ GEM activesupport rack (>= 1.1) rubocop (>= 0.72.0) - ruby-progressbar (1.11.0) + ruby-progressbar (1.12.0) sass (3.2.19) sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) @@ -152,7 +156,7 @@ GEM sprockets-rails (~> 2.0) sassc (2.4.0) ffi (~> 1.9) - sdoc (2.6.0) + sdoc (2.6.1) rdoc (>= 5.0) slop (3.6.0) spring (1.7.2) @@ -191,9 +195,11 @@ DEPENDENCIES cancancan (~> 1.17) coffee-rails (~> 4.0.0) devise + doorkeeper (= 2.0) ffi (~> 1.9, >= 1.9.10) font-awesome-rails font-awesome-sass (~> 6.2.1) + foreigner friendly_numbers jbuilder (~> 1.2) jquery-rails diff --git a/app/controllers/api/v1/board_sections_controller.rb b/app/controllers/api/v1/board_sections_controller.rb index e45e531..f499f19 100644 --- a/app/controllers/api/v1/board_sections_controller.rb +++ b/app/controllers/api/v1/board_sections_controller.rb @@ -3,6 +3,7 @@ module Api module V1 class BoardSectionsController < ActionController::Base + before_action :doorkeeper_authorize! before_action :set_board_section_object, only: %i[show edit update delete] before_action :set_board_object, only: %i[create] diff --git a/app/controllers/api/v1/boards_controller.rb b/app/controllers/api/v1/boards_controller.rb index 122b74c..fb7ff9a 100644 --- a/app/controllers/api/v1/boards_controller.rb +++ b/app/controllers/api/v1/boards_controller.rb @@ -3,8 +3,9 @@ module Api module V1 class BoardsController < ActionController::Base + before_action :doorkeeper_authorize!, unless: :user_signed_in? before_action :set_board_object, only: %i[show update destroy] - before_action :set_user_object, only: %i[create] + before_action :set_user_object, only: %i[create show] def index @boards = Board.all @@ -48,8 +49,11 @@ def set_board_object end def set_user_object - @user = User.find_by(id: board_params[:user_id]) - render json: {message: "User not found"}, status: :unprocessable_entity unless @user.present? + @current_user ||= if doorkeeper_token + User.find(doorkeeper_token.resource_owner_id) + else + warden.authenticate(scope: :user) + end end def board_params diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb index 60fb4c5..a8953d7 100644 --- a/app/controllers/api/v1/tasks_controller.rb +++ b/app/controllers/api/v1/tasks_controller.rb @@ -3,6 +3,7 @@ module Api module V1 class TasksController < ActionController::Base + before_action :doorkeeper_authorize!, unless: :user_signed_in? before_action :set_task, only: %i[show update destroy assign_task] before_action :set_board_section_object, only: %i[create] before_action :set_user, only: [:assign_task] @@ -58,8 +59,11 @@ def set_task end def set_user - @user = User.find_by(id: params[:id]) - render json: {message: "User not found"}, status: :unprocessable_entity unless @user.present? + @current_user ||= if doorkeeper_token + User.find(doorkeeper_token.resource_owner_id) + else + warden.authenticate(scope: :user) + end end def set_board_section_object diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index af05a8e..c8d4096 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -1,6 +1,7 @@ class BoardsController < ApplicationController before_action :set_board_object, only: %i[show edit update destroy] + def index @boards = Board.all.page(params[:page]) end @@ -50,4 +51,5 @@ def board_params def set_board_object @board = Board.find(params[:id]) end + end diff --git a/app/models/user.rb b/app/models/user.rb index 3be037d..693b838 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,10 +1,13 @@ class User < ActiveRecord::Base - # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable and :omniauthable - has_many :boards - has_many :assigned_tasks - has_many :tasks, through: :assigned_tasks - has_many :comments devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :trackable, :validatable + :recoverable, :rememberable, :validatable + +validates :email, format: URI::MailTo::EMAIL_REGEXP + +# the authenticate method from devise documentation +def self.authenticate(email, password) +user = User.find_for_authentication(email: email) +user&.valid_password?(password) ? user : nil +end + end \ No newline at end of file diff --git a/app/views/boards/create.js.erb b/app/views/boards/create.js.erb index 67207db..3d14c19 100644 --- a/app/views/boards/create.js.erb +++ b/app/views/boards/create.js.erb @@ -1,2 +1,2 @@ $('#add-new-task-modal').modal('toggle'); -$(".board-add").append("<%= escape_javascript render(:partial => 'boards/boards', :locals => {board: @board}) %>"); \ No newline at end of file +$(".board-add").append("<%= escape_javascript render(:partial => 'boards/board', :locals => {board: @board}) %>"); \ No newline at end of file diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..016490e --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,8 @@ +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins '*' + resource '*', + headers: :any, + methods: [:get, :post, :put, :patch, :delete, :options, :head] + end + end \ No newline at end of file diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 0000000..e9d09f8 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,119 @@ +Doorkeeper.configure do + # Change the ORM that doorkeeper will use. + # Currently supported options are :active_record, :mongoid2, :mongoid3, + # :mongoid4, :mongo_mapper + orm :active_record + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + # fail "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" + # Put your resource owner authentication logic here. + # Example implementation: + User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url) + end + + # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) + # end + + # Authorization Code expiration time (default 10 minutes). + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default 2 hours). + # If you want to disable expiration, set this to nil. + # access_token_expires_in 2.hours + + # Assign a custom TTL for implicit grants. + # custom_access_token_expires_in do |oauth_client| + # oauth_client.application.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator + # access_token_generator "::Doorkeeper::JWT" + + # Reuse access token for the same resource owner within an application (disabled by default) + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # reuse_access_token + + # Issue access tokens with refresh token (disabled by default) + # use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter :confirmation => true (default false) if you want to enforce ownership of + # a registered application + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # enable_application_owner :confirmation => false + + # Define access token scopes for your provider + # For more information go to + # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes + # default_scopes :public + # optional_scopes :write, :update + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out the wiki for more information on customization + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out the wiki for more information on customization + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Change the native redirect uri for client apps + # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL + # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # + # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # force_ssl_in_redirect_uri !Rails.env.development? + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + # + # grant_flows %w(authorization_code client_credentials) + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # WWW-Authenticate Realm (default "Doorkeeper"). + # realm "Doorkeeper" + resource_owner_from_credentials do |_routes| + User.authenticate(params[:email], params[:password]) + end + + grant_flows %w(password) + skip_authorization do + true + end + + use_refresh_token + +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 0000000..2501b02 --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,151 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + + mongoid: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + + mongo_mapper: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + redirect_uri: 'Use one line per URI' + native_redirect_uri: 'Use %{native_redirect_uri} for local tests' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'Application Id' + secret: 'Secret' + callback_urls: 'Callback urls' + actions: 'Actions' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorize required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + errors: + messages: + # Common error messages + invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + invalid_redirect_uri: 'The redirect uri included is not valid.' + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + #configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + 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.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + # Password Access token errors + invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + application: + title: 'OAuth authorize required' \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a2ce66e..6703b53 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,7 @@ TaskManager::Application.routes.draw do + use_doorkeeper do + skip_controllers :authorizations, :applications, :authorized_applications + end namespace :api do namespace :v1 do resources :boards, only: [:index, :show, :create, :update, :destroy] do diff --git a/db/migrate/20230302143149_create_doorkeeper_tables.rb b/db/migrate/20230302143149_create_doorkeeper_tables.rb new file mode 100644 index 0000000..39b9a09 --- /dev/null +++ b/db/migrate/20230302143149_create_doorkeeper_tables.rb @@ -0,0 +1,58 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + + # Remove `null: false` if you are planning to use grant flows + # that doesn't require redirect URI to be used during authorization + # like Client Credentials flow or Resource Owner Password. + t.text :redirect_uri + t.string :scopes, null: false, default: '' + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, index: true + + # Remove `null: false` if you are planning to use Password + # Credentials Grant flow that doesn't require an application. + t.references :application + + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + + # The authorization server MAY issue a new refresh token, in which case + # *the client MUST discard the old refresh token* and replace it with the + # new refresh token. The authorization server MAY revoke the old + # refresh token after issuing a new refresh token to the client. + # @see https://tools.ietf.org/html/rfc6749#section-6 + # + # Doorkeeper implementation: if there is a `previous_refresh_token` column, + # refresh tokens will be revoked after a related access token is used. + # If there is no `previous_refresh_token` column, previous tokens are + # revoked as soon as a new access token is created. + # + # Comment out this line if you want refresh tokens to be instantly + # revoked after use. + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :refresh_token, unique: true + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e8..6d32a97 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,8 @@ # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) +if Doorkeeper::Application.count.zero? + Doorkeeper::Application.create(name: "iOS client", redirect_uri: "", scopes: "") + Doorkeeper::Application.create(name: "Android client", redirect_uri: "", scopes: "") + Doorkeeper::Application.create(name: "React", redirect_uri: "", scopes: "") +end \ No newline at end of file diff --git a/package.json b/package.json index 2cb4c65..7b623f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "dependencies": { - "bootstrap": "^5.2.3", - "jquery": "^3.6.2", - "popper.js": "^1.16.1" - } -} + "dependencies": { + "bootstrap": "^5.2.3", + "jquery": "^3.6.2", + "popper.js": "^1.16.1" + } +} \ No newline at end of file